New changes and improvemenets (#1437)

pull/1448/head
Ahmed Hnewa 2 years ago committed by GitHub
parent db143c9556
commit 988c41bfa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 54
      CHANGELOG.md
  2. 35
      README.md
  3. 2
      example/android/app/build.gradle
  4. 8
      example/android/app/src/main/AndroidManifest.xml
  5. 24
      example/lib/pages/home_page.dart
  6. 71
      flutter_quill_extensions/lib/embeds/builders.dart
  7. 6
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  8. 54
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  9. 93
      flutter_quill_extensions/lib/embeds/toolbar/media_button.dart
  10. 27
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  11. 2
      flutter_quill_extensions/lib/utils/quill_utils.dart
  12. 6
      flutter_quill_extensions/pubspec.yaml
  13. 2
      lib/src/models/documents/nodes/line.dart
  14. 4
      lib/src/models/documents/nodes/node.dart
  15. 8
      lib/src/models/documents/style.dart
  16. 36
      lib/src/models/rules/format.dart
  17. 121
      lib/src/models/rules/insert.dart
  18. 27
      lib/src/models/rules/rule.dart
  19. 101
      lib/src/translations/toolbar.i18n.dart
  20. 6
      lib/src/utils/platform.dart
  21. 14
      lib/src/widgets/controller.dart
  22. 9
      lib/src/widgets/default_styles.dart
  23. 13
      lib/src/widgets/editor.dart
  24. 197
      lib/src/widgets/raw_editor.dart
  25. 76
      lib/src/widgets/text_block.dart
  26. 85
      lib/src/widgets/toolbar/link_style_button.dart
  27. 2
      lib/src/widgets/toolbar/link_style_button2.dart
  28. 3
      lib/src/widgets/toolbar/quill_icon_button.dart
  29. 16
      lib/src/widgets/toolbar/search_dialog.dart
  30. 34
      test/widgets/controller_test.dart

@ -1,157 +1,210 @@
# [7.4.14] # [7.4.14]
- Custom style attrbuites for platforms other than mobile (alignment, margin, width, height) - 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)` - 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] # [7.4.13]
- Fixed tab editing when in readOnly mode. - Fixed tab editing when in readOnly mode.
# [7.4.12] # [7.4.12]
- Update the minimum version of device_info_plus to 9.1.0. - Update the minimum version of device_info_plus to 9.1.0.
# [7.4.11] # [7.4.11]
- Add sw locale. - Add sw locale.
# [7.4.10] # [7.4.10]
- Update translations. - Update translations.
# [7.4.9] # [7.4.9]
- Style recognition fixes. - Style recognition fixes.
# [7.4.8] # [7.4.8]
- Upgrade dependencies. - Upgrade dependencies.
# [7.4.7] # [7.4.7]
- Add Vietnamese and German translations. - Add Vietnamese and German translations.
# [7.4.6] # [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). - 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] # [7.4.5]
- Fix null error in Container.insert [#1392](https://github.com/singerdmx/flutter-quill/issues/1392). - Fix null error in Container.insert [#1392](https://github.com/singerdmx/flutter-quill/issues/1392).
# [7.4.4] # [7.4.4]
- Fix extra padding on checklists [#1131](https://github.com/singerdmx/flutter-quill/issues/1131). - Fix extra padding on checklists [#1131](https://github.com/singerdmx/flutter-quill/issues/1131).
# [7.4.3] # [7.4.3]
- Fixed a space input error on iPad. - Fixed a space input error on iPad.
# [7.4.2] # [7.4.2]
- Fix bug with keepStyleOnNewLine for link. - Fix bug with keepStyleOnNewLine for link.
# [7.4.1] # [7.4.1]
- Fix toolbar dividers condition. - Fix toolbar dividers condition.
# [7.4.0] # [7.4.0]
- Support Flutter version 3.13.0. - Support Flutter version 3.13.0.
# [7.3.3] # [7.3.3]
- Updated Dependencies conflicting. - Updated Dependencies conflicting.
# [7.3.2] # [7.3.2]
- Added builder for custom button in _LinkDialog. - Added builder for custom button in _LinkDialog.
# [7.3.1] # [7.3.1]
- Added case sensitive and whole word search parameters. - Added case sensitive and whole word search parameters.
- Added wrap around. - Added wrap around.
- Moved search dialog to the bottom in order not to override the editor and the text found. - Moved search dialog to the bottom in order not to override the editor and the text found.
- Other minor search dialog enhancements. - Other minor search dialog enhancements.
# [7.3.0] # [7.3.0]
- Add default attributes to basic factory. - Add default attributes to basic factory.
# [7.2.19] # [7.2.19]
- Feat/link regexp. - Feat/link regexp.
# [7.2.18] # [7.2.18]
- Fix paste block text in words apply same style. - Fix paste block text in words apply same style.
# [7.2.17] # [7.2.17]
- Fix paste text mess up style. - Fix paste text mess up style.
- Add support copy/cut block text. - Add support copy/cut block text.
# [7.2.16] # [7.2.16]
- Allow for custom context menu. - Allow for custom context menu.
# [7.2.15] # [7.2.15]
- Add flutter_quill.delta library which only exposes Delta datatype. - Add flutter_quill.delta library which only exposes Delta datatype.
# [7.2.14] # [7.2.14]
- Fix errors when the editor is used in the `screenshot` package. - Fix errors when the editor is used in the `screenshot` package.
# [7.2.13] # [7.2.13]
- Fix around image can't delete line break. - Fix around image can't delete line break.
# [7.2.12] # [7.2.12]
- Add support for copy/cut select image and text together. - Add support for copy/cut select image and text together.
# [7.2.11] # [7.2.11]
- Add affinity for localPosition. - Add affinity for localPosition.
# [7.2.10] # [7.2.10]
- LINE._getPlainText queryChild inclusive=false. - LINE._getPlainText queryChild inclusive=false.
# [7.2.9] # [7.2.9]
- Add toPlainText method to `EmbedBuilder`. - Add toPlainText method to `EmbedBuilder`.
# [7.2.8] # [7.2.8]
- Add custom button widget in toolbar. - Add custom button widget in toolbar.
# [7.2.7] # [7.2.7]
- Fix language code of Japan. - Fix language code of Japan.
# [7.2.6] # [7.2.6]
- Style custom toolbar buttons like builtins. - Style custom toolbar buttons like builtins.
# [7.2.5] # [7.2.5]
- Always use text cursor for editor on desktop. - Always use text cursor for editor on desktop.
# [7.2.4] # [7.2.4]
- Fixed keepStyleOnNewLine. - Fixed keepStyleOnNewLine.
# [7.2.3] # [7.2.3]
- Get pixel ratio from view. - Get pixel ratio from view.
# [7.2.2] # [7.2.2]
- Prevent operations on stale editor state. - Prevent operations on stale editor state.
# [7.2.1] # [7.2.1]
- Add support for android keyboard content insertion. - Add support for android keyboard content insertion.
- Enhance color picker, enter hex color and color palette option. - Enhance color picker, enter hex color and color palette option.
# [7.2.0] # [7.2.0]
- Checkboxes, bullet points, and number points are now scaled based on the default paragraph font size. - Checkboxes, bullet points, and number points are now scaled based on the default paragraph font size.
# [7.1.20] # [7.1.20]
- Pass linestyle to embedded block. - Pass linestyle to embedded block.
# [7.1.19] # [7.1.19]
- Fix Rtl leading alignment problem. - Fix Rtl leading alignment problem.
# [7.1.18] # [7.1.18]
- Support flutter latest version. - Support flutter latest version.
# [7.1.17+1] # [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)). - 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] # [7.1.16]
- Fixed subscript key from 'sup' to 'sub'. - Fixed subscript key from 'sup' to 'sub'.
# [7.1.15] # [7.1.15]
- Fixed a bug introduced in 7.1.7 where each section in `QuillToolbar` was displayed on its own line. - Fixed a bug introduced in 7.1.7 where each section in `QuillToolbar` was displayed on its own line.
# [7.1.14] # [7.1.14]
- Add indents change for multiline selection. - Add indents change for multiline selection.
# [7.1.13] # [7.1.13]
- Add custom recognizer. - Add custom recognizer.
# [7.1.12] # [7.1.12]
- Add superscript and subscript styles. - Add superscript and subscript styles.
# [7.1.11] # [7.1.11]
- Add inserting indents for lines of list if text is selected. - Add inserting indents for lines of list if text is selected.
# [7.1.10] # [7.1.10]
- Image embedding tweaks - Image embedding tweaks
- Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working.
- Implement image insert for web (image as base64) - 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) - Use merging shortcuts and actions correclty (if the key combination is the same)
# [7.1.8] # [7.1.8]
- Dropdown tweaks - Dropdown tweaks
- Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. - Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items.
- Remove alignment property as useless. - Remove alignment property as useless.

@ -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. The `QuillToolbar` class lets you customize which formatting options are available.
[Sample Page] provides sample code for advanced usage and configuration. [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 ### Font Size
Within the editor toolbar, a drop-down with font-sizing capabilities is available. This can be enabled or disabled with `showFontSize`. 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 > For this to work, you need to add the appropriate permissions
> to your `Info.plist` and `AndroidManifest.xml` files. > to your `Info.plist` and `AndroidManifest.xml` files.
> >
> See https://github.com/natsuk4ze/gal#-get-started to add the needed lines. > See <https://github.com/natsuk4ze/gal#-get-started> to add the needed lines.
### Custom Size Image for Mobile ### Custom Size Image for Mobile

@ -42,7 +42,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.app" applicationId "com.example.app"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
@ -53,7 +52,6 @@ android {
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

@ -1,7 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app"> package="com.example.app">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:label="app" android:label="app"

@ -190,23 +190,23 @@ class _HomePageState extends State<HomePage> {
onTapUp: (details, p1) { onTapUp: (details, p1) {
return _onTripleClickSelection(); return _onTripleClickSelection();
}, },
customStyles: DefaultStyles( customStyles: const DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( TextStyle(
fontSize: 32, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const VerticalSpacing(16, 0), VerticalSpacing(16, 0),
const VerticalSpacing(0, 0), VerticalSpacing(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: TextStyle(fontSize: 9),
subscript: const TextStyle( subscript: TextStyle(
fontFamily: 'SF-UI-Display', fontFamily: 'SF-UI-Display',
fontFeatures: [FontFeature.subscripts()], fontFeatures: [FontFeature.subscripts()],
), ),
superscript: const TextStyle( superscript: TextStyle(
fontFamily: 'SF-UI-Display', fontFamily: 'SF-UI-Display',
fontFeatures: [FontFeature.superscripts()], fontFeatures: [FontFeature.superscripts()],
), ),
@ -230,18 +230,18 @@ class _HomePageState extends State<HomePage> {
onTapUp: (details, p1) { onTapUp: (details, p1) {
return _onTripleClickSelection(); return _onTripleClickSelection();
}, },
customStyles: DefaultStyles( customStyles: const DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( TextStyle(
fontSize: 32, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const VerticalSpacing(16, 0), VerticalSpacing(16, 0),
const VerticalSpacing(0, 0), VerticalSpacing(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: TextStyle(fontSize: 9),
), ),
embedBuilders: [ embedBuilders: [
...defaultEmbedBuildersWeb, ...defaultEmbedBuildersWeb,

@ -55,10 +55,6 @@ class ImageEmbedBuilder extends EmbedBuilder {
OptionalSize? imageSize; OptionalSize? imageSize;
final style = node.style.attributes['style']; 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) { if (style != null) {
final attrs = base.isMobile() final attrs = base.isMobile()
? base.parseKeyValuePairs(style.value.toString(), { ? base.parseKeyValuePairs(style.value.toString(), {
@ -70,8 +66,8 @@ class ImageEmbedBuilder extends EmbedBuilder {
: base.parseKeyValuePairs(style.value.toString(), { : base.parseKeyValuePairs(style.value.toString(), {
Attribute.width.key, Attribute.width.key,
Attribute.height.key, Attribute.height.key,
marginKey, Attribute.margin,
alignmentKey, Attribute.alignment,
}); });
if (attrs.isNotEmpty) { if (attrs.isNotEmpty) {
final width = double.tryParse( final width = double.tryParse(
@ -88,10 +84,10 @@ class ImageEmbedBuilder extends EmbedBuilder {
); );
final alignment = base.getAlignment(base.isMobile() final alignment = base.getAlignment(base.isMobile()
? attrs[Attribute.mobileAlignment] ? attrs[Attribute.mobileAlignment]
: attrs[alignmentKey]); : attrs[Attribute.alignment]);
final margin = (base.isMobile() final margin = (base.isMobile()
? double.tryParse(Attribute.mobileMargin) ? double.tryParse(Attribute.mobileMargin)
: double.tryParse(marginKey)) ?? : double.tryParse(Attribute.margin)) ??
0.0; 0.0;
assert( assert(
@ -198,57 +194,14 @@ class ImageEmbedBuilder extends EmbedBuilder {
controller, controller,
controller.selection.start, controller.selection.start,
); );
// For desktop
String _replaceStyleStringWithSize( final attr =
String s, base.replaceStyleStringWithSize(
double width, getImageStyleString(controller),
double height, width: w,
) { height: h,
final result = <String, String>{}; isMobile: base.isMobile(),
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,
);
controller controller
..skipRequestKeyboard = true ..skipRequestKeyboard = true
..formatText( ..formatText(

@ -146,11 +146,13 @@ class CameraButton extends StatelessWidget {
break; break;
case MediaPickSetting.Gallery: case MediaPickSetting.Gallery:
throw ArgumentError( 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: case MediaPickSetting.Link:
throw ArgumentError( throw ArgumentError(
'Invalid MediaSetting for the camera button', 'Invalid MediaSetting for the camera button.\n'
'link is not related to camera button',
); );
} }
} }

@ -35,17 +35,22 @@ class LinkDialogState extends State<LinkDialog> {
super.initState(); super.initState();
_link = widget.link ?? ''; _link = widget.link ?? '';
_controller = TextEditingController(text: _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 final defaultLinkNonSecureRegExp = RegExp(
// way to custmize the check, that are not based on RegExp, r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
// I already implemented one so tell me if you are interested 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 @override
// final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure void dispose() {
// _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp; _controller.dispose();
_linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; super.dispose();
} }
@override @override
@ -53,23 +58,29 @@ class LinkDialogState extends State<LinkDialog> {
return AlertDialog( return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField( content: TextField(
keyboardType: TextInputType.multiline, keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null, maxLines: null,
style: widget.dialogTheme?.inputTextStyle, style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Paste a link'.i18n, labelText: 'Paste a link'.i18n,
hintText: 'Please enter a valid image url'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
), ),
autofocus: true, autofocus: true,
onChanged: _linkChanged, onChanged: _linkChanged,
controller: _controller, controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _link.isNotEmpty && _linkRegExp.hasMatch(_link) onPressed: _canPress() ? _applyLink : null,
? _applyLink
: null,
child: Text( child: Text(
'Ok'.i18n, 'Ok'.i18n,
style: widget.dialogTheme?.labelTextStyle, style: widget.dialogTheme?.labelTextStyle,
@ -88,6 +99,10 @@ class LinkDialogState extends State<LinkDialog> {
void _applyLink() { void _applyLink() {
Navigator.pop(context, _link.trim()); Navigator.pop(context, _link.trim());
} }
bool _canPress() {
return _link.isNotEmpty && _linkRegExp.hasMatch(_link);
}
} }
class ImageVideoUtils { class ImageVideoUtils {
@ -179,12 +194,13 @@ class ImageVideoUtils {
/// For video picking logic /// For video picking logic
static Future<void> handleVideoButtonTap( static Future<void> handleVideoButtonTap(
BuildContext context, BuildContext context,
QuillController controller, QuillController controller,
ImageSource videoSource, ImageSource videoSource,
OnVideoPickCallback onVideoPickCallback, OnVideoPickCallback onVideoPickCallback, {
{FilePickImpl? filePickImpl, FilePickImpl? filePickImpl,
WebVideoPickImpl? webVideoPickImpl}) async { WebVideoPickImpl? webVideoPickImpl,
}) async {
final index = controller.selection.baseOffset; final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index; final length = controller.selection.extentOffset - index;

@ -9,6 +9,7 @@ import 'package:flutter_quill/translations.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../embed_types.dart'; import '../embed_types.dart';
import 'image_video_utils.dart';
/// Widget which combines [ImageButton] and [VideButton] widgets. This widget /// Widget which combines [ImageButton] and [VideButton] widgets. This widget
/// has more customization and uses dialog similar to one which is used /// has more customization and uses dialog similar to one which is used
@ -16,6 +17,11 @@ import '../embed_types.dart';
class MediaButton extends StatelessWidget { class MediaButton extends StatelessWidget {
const MediaButton({ const MediaButton({
required this.controller, required this.controller,
required this.onImagePickCallback,
required this.onVideoPickCallback,
required this.filePickImpl,
required this.webImagePickImpl,
required this.webVideoPickImpl,
required this.icon, required this.icon,
this.type = QuillMediaType.image, this.type = QuillMediaType.image,
this.iconSize = kDefaultIconSize, this.iconSize = kDefaultIconSize,
@ -73,6 +79,11 @@ class MediaButton extends StatelessWidget {
final AutovalidateMode autovalidateMode; final AutovalidateMode autovalidateMode;
final String? validationMessage; final String? validationMessage;
final OnImagePickCallback onImagePickCallback;
final FilePickImpl? filePickImpl;
final WebImagePickImpl? webImagePickImpl;
final OnVideoPickCallback onVideoPickCallback;
final WebVideoPickImpl? webVideoPickImpl;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -94,24 +105,48 @@ class MediaButton extends StatelessWidget {
} }
Future<void> _onPressedHandler(BuildContext context) async { Future<void> _onPressedHandler(BuildContext context) async {
if (onMediaPickedCallback != null) { if (onMediaPickedCallback == null) {
final mediaSource = await showDialog<MediaPickSetting>(
context: context,
builder: (_) => MediaSourceSelectorDialog(
dialogTheme: dialogTheme,
galleryButtonText: galleryButtonText,
linkButtonText: linkButtonText,
),
);
if (mediaSource != null) {
if (mediaSource == MediaPickSetting.Gallery) {
await _pickImage();
} else {
_inputLink(context);
}
}
} else {
_inputLink(context); _inputLink(context);
return;
}
final mediaSource = await showDialog<MediaPickSetting>(
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<MediaLinkDialog> {
child: Padding( child: Padding(
padding: padding:
widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16),
child: isWrappable child: Form(
? Wrap( child: isWrappable
alignment: WrapAlignment.center, ? Wrap(
crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center,
runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, crossAxisAlignment: WrapCrossAlignment.center,
children: children, runSpacing: widget.dialogTheme?.runSpacing ?? 0.0,
) children: children,
: Row( )
children: children, : Row(
), children: children,
),
),
), ),
), ),
); );
@ -307,6 +344,8 @@ class _MediaLinkDialogState extends State<MediaLinkDialog> {
void _submitLink() => Navigator.pop(context, _linkController.text); void _submitLink() => Navigator.pop(context, _linkController.text);
String? _validateLink(String? value) { String? _validateLink(String? value) {
// TODO: Use [AutoFormatMultipleLinksRule.oneLineRegExp]
// in the next update
if ((value?.isEmpty ?? false) || if ((value?.isEmpty ?? false) ||
!AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) {
return widget.validationMessage ?? 'That is not a valid URL'; return widget.validationMessage ?? 'That is not a valid URL';

@ -9,6 +9,7 @@ import 'embeds/embed_types.dart';
import 'embeds/toolbar/camera_button.dart'; import 'embeds/toolbar/camera_button.dart';
import 'embeds/toolbar/formula_button.dart'; import 'embeds/toolbar/formula_button.dart';
import 'embeds/toolbar/image_button.dart'; import 'embeds/toolbar/image_button.dart';
import 'embeds/toolbar/media_button.dart';
import 'embeds/toolbar/video_button.dart'; import 'embeds/toolbar/video_button.dart';
export 'embeds/embed_types.dart'; export 'embeds/embed_types.dart';
@ -233,6 +234,7 @@ class FlutterQuillEmbeds {
bool showImageButton = true, bool showImageButton = true,
bool showVideoButton = true, bool showVideoButton = true,
bool showCameraButton = true, bool showCameraButton = true,
bool showImageMediaButton = false,
bool showFormulaButton = false, bool showFormulaButton = false,
String? imageButtonTooltip, String? imageButtonTooltip,
String? videoButtonTooltip, String? videoButtonTooltip,
@ -242,6 +244,7 @@ class FlutterQuillEmbeds {
OnVideoPickCallback? onVideoPickCallback, OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector, MediaPickSettingSelector? mediaPickSettingSelector,
MediaPickSettingSelector? cameraPickSettingSelector, MediaPickSettingSelector? cameraPickSettingSelector,
MediaPickedCallback? onImageMediaPickedCallback,
FilePickImpl? filePickImpl, FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl, WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl, WebVideoPickImpl? webVideoPickImpl,
@ -292,6 +295,28 @@ class FlutterQuillEmbeds {
cameraPickSettingSelector: cameraPickSettingSelector, cameraPickSettingSelector: cameraPickSettingSelector,
iconTheme: iconTheme, 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) if (showFormulaButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => (controller, toolbarIconSize, iconTheme, dialogTheme) =>
FormulaButton( FormulaButton(
@ -301,6 +326,6 @@ class FlutterQuillEmbeds {
controller: controller, controller: controller,
iconTheme: iconTheme, iconTheme: iconTheme,
dialogTheme: dialogTheme, dialogTheme: dialogTheme,
) ),
]; ];
} }

@ -74,8 +74,6 @@ class QuillImageUtilities {
final newImageFileExtensionWithDot = path.extension(cachedImagePath); final newImageFileExtensionWithDot = path.extension(cachedImagePath);
final dateTimeAsString = DateTime.now().toIso8601String(); 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 = final newImageFileName =
'$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot'; '$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot';
final newImagePath = path.join(saveDirectory.path, newImageFileName); final newImagePath = path.join(saveDirectory.path, newImageFileName);

@ -12,7 +12,10 @@ dependencies:
flutter: flutter:
sdk: 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 http: ^1.1.0
image_picker: ">=1.0.4" image_picker: ">=1.0.4"
@ -29,5 +32,4 @@ dev_dependencies:
sdk: flutter sdk: flutter
pedantic: ^1.11.1 pedantic: ^1.11.1
# The following section is specific to Flutter packages.
flutter: flutter:

@ -348,7 +348,7 @@ class Line extends Container<Leaf?> {
/// In essence, it is INTERSECTION of each individual segment's styles /// In essence, it is INTERSECTION of each individual segment's styles
Style collectStyle(int offset, int len) { Style collectStyle(int offset, int len) {
final local = math.min(length - offset, len); final local = math.min(length - offset, len);
var result = Style(); var result = const Style();
final excluded = <Attribute>{}; final excluded = <Attribute>{};
void _handle(Style style) { void _handle(Style style) {

@ -22,7 +22,7 @@ abstract class Node extends LinkedListEntry<Node> {
Container? parent; Container? parent;
Style get style => _style; Style get style => _style;
Style _style = Style(); Style _style = const Style();
/// Returns `true` if this node is the first node in the [parent] list. /// Returns `true` if this node is the first node in the [parent] list.
bool get isFirst => list!.first == this; bool get isFirst => list!.first == this;
@ -78,7 +78,7 @@ abstract class Node extends LinkedListEntry<Node> {
} }
void clearStyle() { void clearStyle() {
_style = Style(); _style = const Style();
} }
@override @override

@ -1,19 +1,21 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart' show immutable;
import 'package:quiver/core.dart'; import 'package:quiver/core.dart';
import 'attribute.dart'; import 'attribute.dart';
/* Collection of style attributes */ /* Collection of style attributes */
@immutable
class Style { class Style {
Style() : _attributes = <String, Attribute>{}; const Style() : _attributes = const <String, Attribute>{};
Style.attr(this._attributes); const Style.attr(this._attributes);
final Map<String, Attribute> _attributes; final Map<String, Attribute> _attributes;
static Style fromJson(Map<String, dynamic>? attributes) { static Style fromJson(Map<String, dynamic>? attributes) {
if (attributes == null) { if (attributes == null) {
return Style(); return const Style();
} }
final result = attributes.map((key, dynamic value) { final result = attributes.map((key, dynamic value) {

@ -23,8 +23,13 @@ class ResolveLineFormatRule extends FormatRule {
const ResolveLineFormatRule(); const ResolveLineFormatRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (attribute!.scope != AttributeScope.BLOCK) { if (attribute!.scope != AttributeScope.BLOCK) {
return null; return null;
} }
@ -108,8 +113,13 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
const FormatLinkAtCaretPositionRule(); const FormatLinkAtCaretPositionRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (attribute!.key != Attribute.link.key || len! > 0) { if (attribute!.key != Attribute.link.key || len! > 0) {
return null; return null;
} }
@ -142,8 +152,13 @@ class ResolveInlineFormatRule extends FormatRule {
const ResolveInlineFormatRule(); const ResolveInlineFormatRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (attribute!.scope != AttributeScope.INLINE) { if (attribute!.scope != AttributeScope.INLINE) {
return null; return null;
} }
@ -182,8 +197,13 @@ class ResolveImageFormatRule extends FormatRule {
const ResolveImageFormatRule(); const ResolveImageFormatRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (attribute == null || attribute.key != Attribute.style.key) { if (attribute == null || attribute.key != Attribute.style.key) {
return null; return null;
} }

@ -27,8 +27,13 @@ class PreserveLineStyleOnSplitRule extends InsertRule {
const PreserveLineStyleOnSplitRule(); const PreserveLineStyleOnSplitRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') { if (data is! String || data != '\n') {
return null; return null;
} }
@ -72,8 +77,13 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
const PreserveBlockStyleOnInsertRule(); const PreserveBlockStyleOnInsertRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || !data.contains('\n')) { if (data is! String || !data.contains('\n')) {
// Only interested in text containing at least one newline character. // Only interested in text containing at least one newline character.
return null; return null;
@ -153,8 +163,13 @@ class AutoExitBlockRule extends InsertRule {
} }
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') { if (data is! String || data != '\n') {
return null; return null;
} }
@ -217,8 +232,13 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
const ResetLineFormatOnNewLineRule(); const ResetLineFormatOnNewLineRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') { if (data is! String || data != '\n') {
return null; return null;
} }
@ -248,8 +268,13 @@ class InsertEmbedsRule extends InsertRule {
const InsertEmbedsRule(); const InsertEmbedsRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is String) { if (data is String) {
return null; return null;
} }
@ -329,8 +354,31 @@ class AutoFormatMultipleLinksRule extends InsertRule {
// http://www.example.com/?action=birds&brass=apparatus // http://www.example.com/?action=birds&brass=apparatus
// https://example.net/ // https://example.net/
// URL generator tool (https://www.randomlists.com/urls) is used. // 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 @override
Delta? applyRule( Delta? applyRule(
@ -339,6 +387,7 @@ class AutoFormatMultipleLinksRule extends InsertRule {
int? len, int? len,
Object? data, Object? data,
Attribute? attribute, Attribute? attribute,
Object? extraData,
}) { }) {
// Only format when inserting text. // Only format when inserting text.
if (data is! String) return null; if (data is! String) return null;
@ -373,8 +422,27 @@ class AutoFormatMultipleLinksRule extends InsertRule {
// Build the segment of affected words. // Build the segment of affected words.
final affectedWords = '$leftWordPart$data$rightWordPart'; 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. // 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 there are no matches, do not apply any format.
if (matches.isEmpty) return null; if (matches.isEmpty) return null;
@ -428,8 +496,13 @@ class AutoFormatLinksRule extends InsertRule {
const AutoFormatLinksRule(); const AutoFormatLinksRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != ' ') { if (data is! String || data != ' ') {
return null; return null;
} }
@ -468,8 +541,13 @@ class PreserveInlineStylesRule extends InsertRule {
const PreserveInlineStylesRule(); const PreserveInlineStylesRule();
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data.contains('\n')) { if (data is! String || data.contains('\n')) {
return null; return null;
} }
@ -514,8 +592,13 @@ class CatchAllInsertRule extends InsertRule {
const CatchAllInsertRule(); const CatchAllInsertRule();
@override @override
Delta applyRule(Delta document, int index, Delta applyRule(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
return Delta() return Delta()
..retain(index + (len ?? 0)) ..retain(index + (len ?? 0))
..insert(data); ..insert(data);

@ -10,19 +10,34 @@ enum RuleType { INSERT, DELETE, FORMAT }
abstract class Rule { abstract class Rule {
const Rule(); const Rule();
Delta? apply(Delta document, int index, Delta? apply(
{int? len, Object? data, Attribute? attribute}) { Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
validateArgs(len, data, attribute); validateArgs(len, data, attribute);
return applyRule(document, index, return applyRule(
len: len, data: data, attribute: attribute); document,
index,
len: len,
data: data,
attribute: attribute,
);
} }
void validateArgs(int? len, Object? data, Attribute? attribute); void validateArgs(int? len, Object? data, Attribute? attribute);
/// Applies heuristic rule to an operation on a [document] and returns /// Applies heuristic rule to an operation on a [document] and returns
/// resulting [Delta]. /// resulting [Delta].
Delta? applyRule(Delta document, int index, Delta? applyRule(
{int? len, Object? data, Attribute? attribute}); Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
});
RuleType get type; RuleType get type;
} }

@ -1,5 +1,7 @@
import 'package:i18n_extension/i18n_extension.dart'; import 'package:i18n_extension/i18n_extension.dart';
// TODO: The translation need to be changed and re-reviewd
extension Localization on String { extension Localization on String {
static final _t = Translations.byLocale('en') + 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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'en_us': {
'Paste a link': 'Paste a link', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ar': {
'Paste a link': 'نسخ الرابط', 'Paste a link': 'نسخ الرابط',
@ -210,6 +218,9 @@ extension Localization on String {
'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'
}, },
'da': { 'da': {
'Paste a link': 'Indsæt link', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'de': {
'Paste a link': 'Link hinzufügen', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'fr': {
'Paste a link': 'Coller un lien', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'zh_cn': {
'Paste a link': '粘贴链接', 'Paste a link': '粘贴链接',
@ -470,6 +490,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'zh_hk': {
'Paste a link': '貼上連結', 'Paste a link': '貼上連結',
@ -535,6 +558,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ja': {
'Paste a link': 'リンクをペースト', 'Paste a link': 'リンクをペースト',
@ -600,6 +626,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ko': {
'Paste a link': '링크를 붙여넣어 주세요.', 'Paste a link': '링크를 붙여넣어 주세요.',
@ -665,6 +694,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ru': {
'Paste a link': 'Вставить ссылку', 'Paste a link': 'Вставить ссылку',
@ -730,6 +762,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'es': {
'Paste a link': 'Pega un enlace', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'tr': {
'Paste a link': 'Bağlantıyı Yapıştır', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'uk': {
'Paste a link': 'Вставити посилання', 'Paste a link': 'Вставити посилання',
@ -925,6 +966,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'pt': {
'Paste a link': 'Colar um link', 'Paste a link': 'Colar um link',
@ -991,6 +1035,9 @@ extension Localization on String {
'Saved using the local storage': 'Saved using the local storage':
'Guardado através do armazenamento local', 'Guardado através do armazenamento local',
'Error while saving image': 'Erro a gravar imagem', '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': { 'pt_br': {
'Paste a link': 'Colar um link', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'pl': {
'Paste a link': 'Wklej link', 'Paste a link': 'Wklej link',
@ -1121,6 +1171,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'vi': {
'Paste a link': 'Chèn liên kết', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ur': {
'Paste a link': 'لنک پیسٹ کریں', 'Paste a link': 'لنک پیسٹ کریں',
@ -1251,6 +1307,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'id': {
'Paste a link': 'Tempel tautan', 'Paste a link': 'Tempel tautan',
@ -1316,6 +1375,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'no': {
'Paste a link': 'Lim inn lenke', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'fa': {
'Paste a link': 'جایگذاری لینک', 'Paste a link': 'جایگذاری لینک',
@ -1446,6 +1511,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'hi': {
'Paste a link': 'िक पट कर', 'Paste a link': 'िक पट कर',
@ -1511,6 +1579,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'nl': {
'Paste a link': 'Plak een link', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'sr': {
'Paste a link': 'Nalepi vezu', 'Paste a link': 'Nalepi vezu',
@ -1641,6 +1715,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'cs': {
'Paste a link': 'Vložit odkaz', '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 the network': 'Uloženo pomocí sítě',
'Saved using local storage': 'Uloženo lokálně', 'Saved using local storage': 'Uloženo lokálně',
'Error while saving image': 'Chyba při ukládání obrázku', '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': { 'he': {
'Paste a link': 'הדבק את הלינק', 'Paste a link': 'הדבק את הלינק',
@ -1774,6 +1854,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'ms': {
'Paste a link': 'Tampal Pautan', 'Paste a link': 'Tampal Pautan',
@ -1839,6 +1922,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'it': {
'Paste a link': 'Incolla un collegamento', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'bn': {
'Paste a link': 'িক পট কর', 'Paste a link': 'িক পট কর',
@ -1972,6 +2061,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'tk': {
'Paste a link': 'Baglanyşygy goýuň', '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 network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'bg': {
'Paste a link': 'Поставете връзка', 'Paste a link': 'Поставете връзка',
@ -2108,6 +2203,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', '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': { 'sw': {
'Paste a link': 'Bandika Kiungo', 'Paste a link': 'Bandika Kiungo',
@ -2176,6 +2274,9 @@ extension Localization on String {
'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao', 'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao',
'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani', 'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani',
'Error while saving image': 'Hitilafu Wakati wa Kuhifadhi Picha', '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'
}, },
}; };

@ -29,6 +29,12 @@ bool isAppleOS([TargetPlatform? targetPlatform]) {
}.contains(targetPlatform); }.contains(targetPlatform);
} }
bool isMacOS([TargetPlatform? targetPlatform]) {
if (kIsWeb) return false;
targetPlatform ??= defaultTargetPlatform;
return TargetPlatform.macOS == targetPlatform;
}
Future<bool> isIOSSimulator() async { Future<bool> isIOSSimulator() async {
if (!isAppleOS()) { if (!isAppleOS()) {
return false; return false;

@ -72,7 +72,7 @@ class QuillController extends ChangeNotifier {
/// Store any styles attribute that got toggled by the tap of a button /// Store any styles attribute that got toggled by the tap of a button
/// and that has not been applied yet. /// and that has not been applied yet.
/// It gets reset after each format action within the [document]. /// It gets reset after each format action within the [document].
Style toggledStyle = Style(); Style toggledStyle = const Style();
bool ignoreFocusOnTextChange = false; bool ignoreFocusOnTextChange = false;
@ -227,8 +227,12 @@ class QuillController extends ChangeNotifier {
} }
void replaceText( void replaceText(
int index, int len, Object? data, TextSelection? textSelection, int index,
{bool ignoreFocus = false}) { int len,
Object? data,
TextSelection? textSelection, {
bool ignoreFocus = false,
}) {
assert(data is String || data is Embeddable); assert(data is String || data is Embeddable);
if (onReplaceText != null && !onReplaceText!(index, len, data)) { if (onReplaceText != null && !onReplaceText!(index, len, data)) {
@ -405,7 +409,7 @@ class QuillController extends ChangeNotifier {
); );
toggledStyle = style.removeAll(ignoredStyles.toSet()); toggledStyle = style.removeAll(ignoredStyles.toSet());
} else { } else {
toggledStyle = Style(); toggledStyle = const Style();
} }
onSelectionChanged?.call(textSelection); onSelectionChanged?.call(textSelection);
} }
@ -426,5 +430,5 @@ class QuillController extends ChangeNotifier {
} }
// Notify toolbar buttons directly with attributes // Notify toolbar buttons directly with attributes
Map<String, Attribute> toolbarButtonToggler = {}; Map<String, Attribute> toolbarButtonToggler = const {};
} }

@ -34,8 +34,9 @@ class QuillStyles extends InheritedWidget {
/// Style theme applied to a block of rich text, including single-line /// Style theme applied to a block of rich text, including single-line
/// paragraphs. /// paragraphs.
@immutable
class DefaultTextBlockStyle { class DefaultTextBlockStyle {
DefaultTextBlockStyle( const DefaultTextBlockStyle(
this.style, this.style,
this.verticalSpacing, this.verticalSpacing,
this.lineSpacing, this.lineSpacing,
@ -124,8 +125,9 @@ class InlineCodeStyle {
Object.hash(style, header1, header2, header3, backgroundColor, radius); Object.hash(style, header1, header2, header3, backgroundColor, radius);
} }
@immutable
class DefaultListBlockStyle extends DefaultTextBlockStyle { class DefaultListBlockStyle extends DefaultTextBlockStyle {
DefaultListBlockStyle( const DefaultListBlockStyle(
TextStyle style, TextStyle style,
VerticalSpacing verticalSpacing, VerticalSpacing verticalSpacing,
VerticalSpacing lineSpacing, VerticalSpacing lineSpacing,
@ -136,8 +138,9 @@ class DefaultListBlockStyle extends DefaultTextBlockStyle {
final QuillCheckboxBuilder? checkboxUIBuilder; final QuillCheckboxBuilder? checkboxUIBuilder;
} }
@immutable
class DefaultStyles { class DefaultStyles {
DefaultStyles({ const DefaultStyles({
this.h1, this.h1,
this.h2, this.h2,
this.h3, this.h3,

@ -156,6 +156,7 @@ class QuillEditor extends StatefulWidget {
required this.autoFocus, required this.autoFocus,
required this.readOnly, required this.readOnly,
required this.expands, required this.expands,
this.textSelectionThemeData,
this.showCursor, this.showCursor,
this.paintCursorAboveText, this.paintCursorAboveText,
this.placeholder, this.placeholder,
@ -199,6 +200,7 @@ class QuillEditor extends StatefulWidget {
factory QuillEditor.basic({ factory QuillEditor.basic({
required QuillController controller, required QuillController controller,
required bool readOnly, required bool readOnly,
TextSelectionThemeData? textSelectionThemeData,
Brightness? keyboardAppearance, Brightness? keyboardAppearance,
Iterable<EmbedBuilder>? embedBuilders, Iterable<EmbedBuilder>? embedBuilders,
EdgeInsetsGeometry padding = EdgeInsets.zero, EdgeInsetsGeometry padding = EdgeInsets.zero,
@ -217,6 +219,7 @@ class QuillEditor extends StatefulWidget {
scrollController: ScrollController(), scrollController: ScrollController(),
scrollable: true, scrollable: true,
focusNode: focusNode ?? FocusNode(), focusNode: focusNode ?? FocusNode(),
textSelectionThemeData: textSelectionThemeData,
autoFocus: autoFocus, autoFocus: autoFocus,
readOnly: readOnly, readOnly: readOnly,
expands: expands, expands: expands,
@ -455,6 +458,13 @@ class QuillEditor extends StatefulWidget {
/// editorKey.currentState?.renderEditor.getLocalRectForCaret /// editorKey.currentState?.renderEditor.getLocalRectForCaret
final GlobalKey<EditorState>? editorKey; final GlobalKey<EditorState>? editorKey;
/// By default we will use
/// ```
/// TextSelectionTheme.of(context)
/// ```
/// to change it please pass a different value
final TextSelectionThemeData? textSelectionThemeData;
@override @override
QuillEditorState createState() => QuillEditorState(); QuillEditorState createState() => QuillEditorState();
} }
@ -477,7 +487,8 @@ class QuillEditorState extends State<QuillEditor>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final selectionTheme = TextSelectionTheme.of(context); final selectionTheme =
widget.textSelectionThemeData ?? TextSelectionTheme.of(context);
TextSelectionControls textSelectionControls; TextSelectionControls textSelectionControls;
bool paintCursorAboveText; bool paintCursorAboveText;

@ -533,9 +533,14 @@ class RawEditorState extends EditorState
? const BoxConstraints.expand() ? const BoxConstraints.expand()
: BoxConstraints( : BoxConstraints(
minHeight: widget.minHeight ?? 0.0, 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( return TextFieldTapRegion(
enabled: widget.enableUnfocusOnTapOutside, enabled: widget.enableUnfocusOnTapOutside,
@ -550,125 +555,125 @@ class RawEditorState extends EditorState
): const HideSelectionToolbarIntent(), ): const HideSelectionToolbarIntent(),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyZ, LogicalKeyboardKey.keyZ,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const UndoTextIntent(SelectionChangedCause.keyboard), ): const UndoTextIntent(SelectionChangedCause.keyboard),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyY, LogicalKeyboardKey.keyY,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const RedoTextIntent(SelectionChangedCause.keyboard), ): const RedoTextIntent(SelectionChangedCause.keyboard),
// Selection formatting. // Selection formatting.
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.bold), ): const ToggleTextStyleIntent(Attribute.bold),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyU, LogicalKeyboardKey.keyU,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.underline), ): const ToggleTextStyleIntent(Attribute.underline),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyI, LogicalKeyboardKey.keyI,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.italic), ): const ToggleTextStyleIntent(Attribute.italic),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyS, LogicalKeyboardKey.keyS,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ToggleTextStyleIntent(Attribute.strikeThrough), ): const ToggleTextStyleIntent(Attribute.strikeThrough),
SingleActivator( SingleActivator(
LogicalKeyboardKey.backquote, LogicalKeyboardKey.backquote,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.inlineCode), ): const ToggleTextStyleIntent(Attribute.inlineCode),
SingleActivator( SingleActivator(
LogicalKeyboardKey.tilde, LogicalKeyboardKey.tilde,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ToggleTextStyleIntent(Attribute.codeBlock), ): const ToggleTextStyleIntent(Attribute.codeBlock),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ToggleTextStyleIntent(Attribute.blockQuote), ): const ToggleTextStyleIntent(Attribute.blockQuote),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyK, LogicalKeyboardKey.keyK,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ApplyLinkIntent(), ): const ApplyLinkIntent(),
// Lists // Lists
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyL, LogicalKeyboardKey.keyL,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ToggleTextStyleIntent(Attribute.ul), ): const ToggleTextStyleIntent(Attribute.ul),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyO, LogicalKeyboardKey.keyO,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ToggleTextStyleIntent(Attribute.ol), ): const ToggleTextStyleIntent(Attribute.ol),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyC,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const ApplyCheckListIntent(), ): const ApplyCheckListIntent(),
// Indents // Indents
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyM, LogicalKeyboardKey.keyM,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const IndentSelectionIntent(true), ): const IndentSelectionIntent(true),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyM, LogicalKeyboardKey.keyM,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
shift: true, shift: true,
): const IndentSelectionIntent(false), ): const IndentSelectionIntent(false),
// Headers // Headers
SingleActivator( SingleActivator(
LogicalKeyboardKey.digit1, LogicalKeyboardKey.digit1,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h1), ): const ApplyHeaderIntent(Attribute.h1),
SingleActivator( SingleActivator(
LogicalKeyboardKey.digit2, LogicalKeyboardKey.digit2,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h2), ): const ApplyHeaderIntent(Attribute.h2),
SingleActivator( SingleActivator(
LogicalKeyboardKey.digit3, LogicalKeyboardKey.digit3,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h3), ): const ApplyHeaderIntent(Attribute.h3),
SingleActivator( SingleActivator(
LogicalKeyboardKey.digit0, LogicalKeyboardKey.digit0,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.header), ): const ApplyHeaderIntent(Attribute.header),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyG, LogicalKeyboardKey.keyG,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const InsertEmbedIntent(Attribute.image), ): const InsertEmbedIntent(Attribute.image),
SingleActivator( SingleActivator(
LogicalKeyboardKey.keyF, LogicalKeyboardKey.keyF,
control: !isMacOS, control: !isDesktopMacOS,
meta: isMacOS, meta: isDesktopMacOS,
): const OpenSearchIntent(), ): const OpenSearchIntent(),
}, { }, {
...?widget.customShortcuts ...?widget.customShortcuts
@ -915,31 +920,36 @@ class RawEditorState extends EditorState
textDirection: getDirectionOfNode(node), child: editableTextLine)); textDirection: getDirectionOfNode(node), child: editableTextLine));
} else if (node is Block) { } else if (node is Block) {
final editableTextBlock = EditableTextBlock( final editableTextBlock = EditableTextBlock(
block: node, block: node,
controller: controller, 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), textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset, child: editableTextBlock,
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));
clearIndents = false; clearIndents = false;
} else { } else {
@ -1111,9 +1121,15 @@ class RawEditorState extends EditorState
_styles = _styles!.merge(widget.customStyles!); _styles = _styles!.merge(widget.customStyles!);
} }
// TODO: this might need some attention
_requestFocusIfShould();
}
Future<void> _requestFocusIfShould() async {
if (!_didAutoFocus && widget.autoFocus) { if (!_didAutoFocus && widget.autoFocus) {
FocusScope.of(context).autofocus(widget.focusNode);
_didAutoFocus = true; _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 index = textEditingValue.selection.baseOffset;
final length = textEditingValue.selection.extentOffset - index; final length = textEditingValue.selection.extentOffset - index;
final copied = controller.copiedImageUrl!; 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) { if (copied.styleString.isNotEmpty) {
controller.formatText(getEmbedNode(controller, index + 1).offset, 1, controller.formatText(
StyleAttribute(copied.styleString)); getEmbedNode(controller, index + 1).offset,
1,
StyleAttribute(copied.styleString),
);
} }
controller.copiedImageUrl = null; controller.copiedImageUrl = null;
await Clipboard.setData(const ClipboardData(text: '')); await Clipboard.setData(
const ClipboardData(text: ''),
);
return; return;
} }
@ -1516,7 +1542,13 @@ class RawEditorState extends EditorState
final text = await Clipboard.getData(Clipboard.kTextPlain); final text = await Clipboard.getData(Clipboard.kTextPlain);
if (text != null) { if (text != null) {
_replaceText( _replaceText(
ReplaceTextIntent(textEditingValue, text.text!, selection, cause)); ReplaceTextIntent(
textEditingValue,
text.text!,
selection,
cause,
),
);
bringIntoView(textEditingValue.selection.extent); bringIntoView(textEditingValue.selection.extent);
@ -1524,8 +1556,9 @@ class RawEditorState extends EditorState
userUpdateTextEditingValue( userUpdateTextEditingValue(
TextEditingValue( TextEditingValue(
text: textEditingValue.text, text: textEditingValue.text,
selection: selection: TextSelection.collapsed(
TextSelection.collapsed(offset: textEditingValue.selection.end), offset: textEditingValue.selection.end,
),
), ),
cause, cause,
); );
@ -1533,14 +1566,15 @@ class RawEditorState extends EditorState
return; return;
} }
if (widget.onImagePaste != null) { final onImagePaste = widget.onImagePaste;
if (onImagePaste != null) {
final image = await Pasteboard.image; final image = await Pasteboard.image;
if (image == null) { if (image == null) {
return; return;
} }
final imageUrl = await widget.onImagePaste!(image); final imageUrl = await onImagePaste(image);
if (imageUrl == null) { if (imageUrl == null) {
return; return;
} }
@ -2559,8 +2593,13 @@ class _OpenSearchAction extends ContextAction<OpenSearchIntent> {
@override @override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async { Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
if (context == null) {
throw ArgumentError(
'The context should not be null to use invoke() method',
);
}
await showDialog<String>( await showDialog<String>(
context: context!, context: context,
builder: (_) => SearchDialog(controller: state.controller, text: ''), builder: (_) => SearchDialog(controller: state.controller, text: ''),
); );
} }

@ -104,14 +104,19 @@ class EditableTextBlock extends StatelessWidget {
final defaultStyles = QuillStyles.getStyles(context, false); final defaultStyles = QuillStyles.getStyles(context, false);
return _EditableBlock( return _EditableBlock(
block: block, block: block,
textDirection: textDirection, textDirection: textDirection,
padding: verticalSpacing, padding: verticalSpacing,
scrollBottomInset: scrollBottomInset, scrollBottomInset: scrollBottomInset,
decoration: _getDecorationForBlock(block, defaultStyles) ?? decoration:
const BoxDecoration(), _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(),
contentPadding: contentPadding, contentPadding: contentPadding,
children: _buildChildren(context, indentLevelCounts, clearIndents)); children: _buildChildren(
context,
indentLevelCounts,
clearIndents,
),
);
} }
BoxDecoration? _getDecorationForBlock( BoxDecoration? _getDecorationForBlock(
@ -138,32 +143,37 @@ class EditableTextBlock extends StatelessWidget {
for (final line in Iterable.castFrom<dynamic, Line>(block.children)) { for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
index++; index++;
final editableTextLine = EditableTextLine( final editableTextLine = EditableTextLine(
line, line,
_buildLeading(context, line, index, indentLevelCounts, count), _buildLeading(context, line, index, indentLevelCounts, count),
TextLine( TextLine(
line: line, line: line,
textDirection: textDirection, textDirection: textDirection,
embedBuilder: embedBuilder, embedBuilder: embedBuilder,
customStyleBuilder: customStyleBuilder, customStyleBuilder: customStyleBuilder,
styles: styles!, styles: styles!,
readOnly: readOnly, readOnly: readOnly,
controller: controller, controller: controller,
linkActionPicker: linkActionPicker, linkActionPicker: linkActionPicker,
onLaunchUrl: onLaunchUrl, onLaunchUrl: onLaunchUrl,
customLinkPrefixes: customLinkPrefixes, customLinkPrefixes: customLinkPrefixes,
), ),
_getIndentWidth(context, count), _getIndentWidth(context, count),
_getSpacingForLine(line, index, count, defaultStyles), _getSpacingForLine(line, index, count, defaultStyles),
textDirection, textDirection,
textSelection, textSelection,
color, color,
enableInteractiveSelection, enableInteractiveSelection,
hasFocus, hasFocus,
MediaQuery.devicePixelRatioOf(context), MediaQuery.devicePixelRatioOf(context),
cursorCont); cursorCont,
);
final nodeTextDirection = getDirectionOfNode(line); final nodeTextDirection = getDirectionOfNode(line);
children.add(Directionality( children.add(
textDirection: nodeTextDirection, child: editableTextLine)); Directionality(
textDirection: nodeTextDirection,
child: editableTextLine,
),
);
} }
return children.toList(growable: false); return children.toList(growable: false);
} }

@ -184,51 +184,82 @@ class _LinkDialogState extends State<_LinkDialog> {
super.initState(); super.initState();
_link = widget.link ?? ''; _link = widget.link ?? '';
_text = widget.text ?? ''; _text = widget.text ?? '';
linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.oneLineRegExp;
_linkController = TextEditingController(text: _link); _linkController = TextEditingController(text: _link);
_textController = TextEditingController(text: _text); _textController = TextEditingController(text: _text);
} }
@override
void dispose() {
_linkController.dispose();
_textController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: Column( content: Form(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
const SizedBox(height: 8), children: [
TextField( const SizedBox(height: 8),
keyboardType: TextInputType.multiline, TextFormField(
style: widget.dialogTheme?.inputTextStyle, keyboardType: TextInputType.text,
decoration: InputDecoration( style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Text'.i18n, labelText: 'Text'.i18n,
hintText: 'Please enter a text for your link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
autofocus: true, ),
onChanged: _textChanged, autofocus: true,
controller: _textController, onChanged: _textChanged,
), controller: _textController,
const SizedBox(height: 16), textInputAction: TextInputAction.next,
TextField( autofillHints: [
keyboardType: TextInputType.multiline, AutofillHints.name,
style: widget.dialogTheme?.inputTextStyle, AutofillHints.url,
decoration: InputDecoration( ],
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.url,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Link'.i18n, labelText: 'Link'.i18n,
hintText: 'Please enter the link url'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
autofocus: true, ),
onChanged: _linkChanged, onChanged: _linkChanged,
controller: _linkController, controller: _linkController,
), textInputAction: TextInputAction.done,
], autofillHints: [AutofillHints.url],
autocorrect: false,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
],
),
), ),
actions: [_okButton()], actions: [
_okButton(),
],
); );
} }
Widget _okButton() { Widget _okButton() {
if (widget.action != null) { if (widget.action != null) {
return widget.action!.builder(_canPress(), _applyLink); return widget.action!.builder(
_canPress(),
_applyLink,
);
} }
return TextButton( return TextButton(

@ -379,7 +379,7 @@ class _LinkStyleDialogState extends State<LinkStyleDialog> {
String? _validateLink(String? value) { String? _validateLink(String? value) {
if ((value?.isEmpty ?? false) || if ((value?.isEmpty ?? false) ||
!AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { !AutoFormatMultipleLinksRule.oneLineRegExp.hasMatch(value!)) {
return widget.validationMessage ?? 'That is not a valid URL'; return widget.validationMessage ?? 'That is not a valid URL';
} }

@ -35,7 +35,8 @@ class QuillIconButton extends StatelessWidget {
child: RawMaterialButton( child: RawMaterialButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius)), borderRadius: BorderRadius.circular(borderRadius),
),
fillColor: fillColor, fillColor: fillColor,
elevation: 0, elevation: 0,
hoverElevation: hoverElevation, hoverElevation: hoverElevation,

@ -6,9 +6,12 @@ import '../../models/themes/quill_dialog_theme.dart';
import '../controller.dart'; import '../controller.dart';
class SearchDialog extends StatefulWidget { class SearchDialog extends StatefulWidget {
const SearchDialog( const SearchDialog({
{required this.controller, this.dialogTheme, this.text, Key? key}) required this.controller,
: super(key: key); this.dialogTheme,
this.text,
Key? key,
}) : super(key: key);
final QuillController controller; final QuillController controller;
final QuillDialogTheme? dialogTheme; final QuillDialogTheme? dialogTheme;
@ -35,6 +38,12 @@ class _SearchDialogState extends State<SearchDialog> {
_controller = TextEditingController(text: _text); _controller = TextEditingController(text: _text);
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var matchShown = ''; var matchShown = '';
@ -100,6 +109,7 @@ class _SearchDialogState extends State<SearchDialog> {
autofocus: true, autofocus: true,
onChanged: _textChanged, onChanged: _textChanged,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
keyboardType: TextInputType.text,
onEditingComplete: _findText, onEditingComplete: _findText,
controller: _controller, controller: _controller,
), ),

@ -89,8 +89,12 @@ void main() {
..indentSelection(true); ..indentSelection(true);
// Should have both L1 and L2 indent attributes in selection. // Should have both L1 and L2 indent attributes in selection.
expect(controller.getAllSelectionStyles(), expect(
contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); controller.getAllSelectionStyles(),
contains(
const Style().put(Attribute.indentL1).put(Attribute.indentL2),
),
);
// Remaining lines should have no attributes. // Remaining lines should have no attributes.
controller.updateSelection( controller.updateSelection(
@ -98,7 +102,7 @@ void main() {
baseOffset: 12, baseOffset: 12,
extentOffset: controller.document.toPlainText().length - 1), extentOffset: controller.document.toPlainText().length - 1),
ChangeSource.LOCAL); ChangeSource.LOCAL);
expect(controller.getAllSelectionStyles(), everyElement(Style())); expect(controller.getAllSelectionStyles(), everyElement(const Style()));
}); });
test('getAllIndividualSelectionStylesAndEmbed', () { test('getAllIndividualSelectionStylesAndEmbed', () {
@ -110,7 +114,7 @@ void main() {
final result = controller.getAllIndividualSelectionStylesAndEmbed(); final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2); expect(result.length, 2);
expect(result[0].offset, 0); 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); expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
}); });
@ -125,7 +129,7 @@ void main() {
test('getAllSelectionStyles', () { test('getAllSelectionStyles', () {
controller.formatText(0, 2, Attribute.bold); controller.formatText(0, 2, Attribute.bold);
expect(controller.getAllSelectionStyles(), expect(controller.getAllSelectionStyles(),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
}); });
test('undo', () { test('undo', () {
@ -133,7 +137,10 @@ void main() {
controller.updateSelection( controller.updateSelection(
const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL);
expect(controller.document.toDelta(), Delta()..insert('data\n')); expect(
controller.document.toDelta(),
Delta()..insert('data\n'),
);
controller controller
..addListener(() { ..addListener(() {
listenerCalled = true; listenerCalled = true;
@ -185,7 +192,7 @@ void main() {
test('formatTextStyle', () { test('formatTextStyle', () {
var listenerCalled = false; var listenerCalled = false;
final style = Style().put(Attribute.bold).put(Attribute.italic); final style = const Style().put(Attribute.bold).put(Attribute.italic);
controller controller
..addListener(() { ..addListener(() {
listenerCalled = true; listenerCalled = true;
@ -193,7 +200,8 @@ void main() {
..formatTextStyle(0, 2, style); ..formatTextStyle(0, 2, style);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), contains(style)); 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', () { test('formatText', () {
@ -205,8 +213,9 @@ void main() {
..formatText(0, 2, Attribute.bold); ..formatText(0, 2, Attribute.bold);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), expect(controller.document.collectAllStyles(0, 2),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
}); });
test('formatSelection', () { test('formatSelection', () {
@ -220,8 +229,9 @@ void main() {
..formatSelection(Attribute.bold); ..formatSelection(Attribute.bold);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), expect(controller.document.collectAllStyles(0, 2),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
}); });
test('moveCursorToStart', () { test('moveCursorToStart', () {

Loading…
Cancel
Save