New changes and improvemenets (#1437)

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

@ -111,6 +111,39 @@ It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample
The `QuillToolbar` class lets you customize which formatting options are available.
[Sample Page] provides sample code for advanced usage and configuration.
### Using Custom App Widget
This project use some adaptive widgets like `AdaptiveTextSelectionToolbar` which require the following delegates:
1. Default Material Localizations delegate
2. Default Cupertino Localizations delegate
3. Defualt Widgets Localizations delegate
You don't need to include those since there are defined by default
but if you are using Custom app or you are overriding the `localizationsDelegates` in the App widget
then please make sure it's including those:
```dart
localizationsDelegates: const [
DefaultCupertinoLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
```
And you might need more depending on your use case, for example if you are using custom localizations for your app, using custom app widget like [FluentApp](https://pub.dev/packages/fluent_ui)
which will also need
```dart
localizationsDelegates: const [
// Required localizations delegates ...
FluentLocalizations.delegate,
AppLocalizations.delegate,
],
```
in addition to the required delegates by this library
### Font Size
Within the editor toolbar, a drop-down with font-sizing capabilities is available. This can be enabled or disabled with `showFontSize`.
@ -203,7 +236,7 @@ QuillToolbar.basic(
> For this to work, you need to add the appropriate permissions
> to your `Info.plist` and `AndroidManifest.xml` files.
>
> See https://github.com/natsuk4ze/gal#-get-started to add the needed lines.
> See <https://github.com/natsuk4ze/gal#-get-started> to add the needed lines.
### Custom Size Image for Mobile

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

@ -1,7 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<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
android:name="${applicationName}"
android:label="app"

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

@ -55,10 +55,6 @@ class ImageEmbedBuilder extends EmbedBuilder {
OptionalSize? imageSize;
final style = node.style.attributes['style'];
// TODO: Please use the one from [Attribute.margin]
const marginKey = 'margin';
// TODO: Please use the one from [Attribute.alignment]
const alignmentKey = 'alignment';
if (style != null) {
final attrs = base.isMobile()
? base.parseKeyValuePairs(style.value.toString(), {
@ -70,8 +66,8 @@ class ImageEmbedBuilder extends EmbedBuilder {
: base.parseKeyValuePairs(style.value.toString(), {
Attribute.width.key,
Attribute.height.key,
marginKey,
alignmentKey,
Attribute.margin,
Attribute.alignment,
});
if (attrs.isNotEmpty) {
final width = double.tryParse(
@ -88,10 +84,10 @@ class ImageEmbedBuilder extends EmbedBuilder {
);
final alignment = base.getAlignment(base.isMobile()
? attrs[Attribute.mobileAlignment]
: attrs[alignmentKey]);
: attrs[Attribute.alignment]);
final margin = (base.isMobile()
? double.tryParse(Attribute.mobileMargin)
: double.tryParse(marginKey)) ??
: double.tryParse(Attribute.margin)) ??
0.0;
assert(
@ -198,57 +194,14 @@ class ImageEmbedBuilder extends EmbedBuilder {
controller,
controller.selection.start,
);
// For desktop
String _replaceStyleStringWithSize(
String s,
double width,
double height,
) {
final result = <String, String>{};
final pairs = s.split(';');
for (final pair in pairs) {
final _index = pair.indexOf(':');
if (_index < 0) {
continue;
}
final _key =
pair.substring(0, _index).trim();
result[_key] =
pair.substring(_index + 1).trim();
}
result[Attribute.width.key] =
width.toString();
result[Attribute.height.key] =
height.toString();
final sb = StringBuffer();
for (final pair in result.entries) {
sb
..write(pair.key)
..write(': ')
..write(pair.value)
..write('; ');
}
return sb.toString();
}
// TODO: When update flutter_quill
// we should update flutter_quill_extensions
// to use the latest version and use
// base.replaceStyleStringWithSize()
// instead of replaceStyleString
final attr = base.isMobile()
? base.replaceStyleString(
getImageStyleString(controller),
w,
h,
)
: _replaceStyleStringWithSize(
getImageStyleString(controller),
w,
h,
);
final attr =
base.replaceStyleStringWithSize(
getImageStyleString(controller),
width: w,
height: h,
isMobile: base.isMobile(),
);
controller
..skipRequestKeyboard = true
..formatText(

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

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

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

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

@ -74,8 +74,6 @@ class QuillImageUtilities {
final newImageFileExtensionWithDot = path.extension(cachedImagePath);
final dateTimeAsString = DateTime.now().toIso8601String();
// TODO: You might want to make it easier for the developer to change
// the newImageFileName, but he can rename it anyway
final newImageFileName =
'$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot';
final newImagePath = path.join(saveDirectory.path, newImageFileName);

@ -12,7 +12,10 @@ dependencies:
flutter:
sdk: flutter
flutter_quill: ^7.4.13
flutter_quill: ^7.4.14
# In case you are working on changes for both libraries,
# flutter_quill:
# path: ~/development/playground/framework_based/flutter/flutter-quill
http: ^1.1.0
image_picker: ">=1.0.4"
@ -29,5 +32,4 @@ dev_dependencies:
sdk: flutter
pedantic: ^1.11.1
# The following section is specific to Flutter packages.
flutter:

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

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

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

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

@ -27,8 +27,13 @@ class PreserveLineStyleOnSplitRule extends InsertRule {
const PreserveLineStyleOnSplitRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') {
return null;
}
@ -72,8 +77,13 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
const PreserveBlockStyleOnInsertRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || !data.contains('\n')) {
// Only interested in text containing at least one newline character.
return null;
@ -153,8 +163,13 @@ class AutoExitBlockRule extends InsertRule {
}
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') {
return null;
}
@ -217,8 +232,13 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
const ResetLineFormatOnNewLineRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != '\n') {
return null;
}
@ -248,8 +268,13 @@ class InsertEmbedsRule extends InsertRule {
const InsertEmbedsRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is String) {
return null;
}
@ -329,8 +354,31 @@ class AutoFormatMultipleLinksRule extends InsertRule {
// http://www.example.com/?action=birds&brass=apparatus
// https://example.net/
// URL generator tool (https://www.randomlists.com/urls) is used.
static const _linkPattern = r'^https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/.*)?$';
static final linkRegExp = RegExp(_linkPattern, caseSensitive: false);
// TODO: You might want to rename those but everywhere even in
// flutter_quill_extensions
static const _oneLinePattern =
r'^https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/.*)?$';
static const _detectLinkPattern =
r'https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/[^\s]*)?';
/// It requires a valid link in one link
static final oneLineRegExp = RegExp(
_oneLinePattern,
caseSensitive: false,
);
/// It detect if there is a link in the text whatever if it in the middle etc
// Used to solve bug https://github.com/singerdmx/flutter-quill/issues/1432
static final detectLinkRegExp = RegExp(
_detectLinkPattern,
caseSensitive: false,
);
@Deprecated(
'Please use [linkRegExp1] or [linkRegExp2]',
)
static final linkRegExp = oneLineRegExp;
@override
Delta? applyRule(
@ -339,6 +387,7 @@ class AutoFormatMultipleLinksRule extends InsertRule {
int? len,
Object? data,
Attribute? attribute,
Object? extraData,
}) {
// Only format when inserting text.
if (data is! String) return null;
@ -373,8 +422,27 @@ class AutoFormatMultipleLinksRule extends InsertRule {
// Build the segment of affected words.
final affectedWords = '$leftWordPart$data$rightWordPart';
var usedRegExp = detectLinkRegExp;
final alternativeLinkRegExp = extraData;
if (alternativeLinkRegExp != null) {
try {
if (alternativeLinkRegExp is! String) {
throw ArgumentError.value(
alternativeLinkRegExp,
'alternativeLinkRegExp',
'`alternativeLinkRegExp` should be of type String',
);
}
final regPattern = alternativeLinkRegExp;
usedRegExp = RegExp(
regPattern,
caseSensitive: false,
);
} catch (_) {}
}
// Check for URL pattern.
final matches = linkRegExp.allMatches(affectedWords);
final matches = usedRegExp.allMatches(affectedWords);
// If there are no matches, do not apply any format.
if (matches.isEmpty) return null;
@ -428,8 +496,13 @@ class AutoFormatLinksRule extends InsertRule {
const AutoFormatLinksRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data != ' ') {
return null;
}
@ -468,8 +541,13 @@ class PreserveInlineStylesRule extends InsertRule {
const PreserveInlineStylesRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta? applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
if (data is! String || data.contains('\n')) {
return null;
}
@ -514,8 +592,13 @@ class CatchAllInsertRule extends InsertRule {
const CatchAllInsertRule();
@override
Delta applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
Delta applyRule(
Delta document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
return Delta()
..retain(index + (len ?? 0))
..insert(data);

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

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

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

@ -72,7 +72,7 @@ class QuillController extends ChangeNotifier {
/// Store any styles attribute that got toggled by the tap of a button
/// and that has not been applied yet.
/// It gets reset after each format action within the [document].
Style toggledStyle = Style();
Style toggledStyle = const Style();
bool ignoreFocusOnTextChange = false;
@ -227,8 +227,12 @@ class QuillController extends ChangeNotifier {
}
void replaceText(
int index, int len, Object? data, TextSelection? textSelection,
{bool ignoreFocus = false}) {
int index,
int len,
Object? data,
TextSelection? textSelection, {
bool ignoreFocus = false,
}) {
assert(data is String || data is Embeddable);
if (onReplaceText != null && !onReplaceText!(index, len, data)) {
@ -405,7 +409,7 @@ class QuillController extends ChangeNotifier {
);
toggledStyle = style.removeAll(ignoredStyles.toSet());
} else {
toggledStyle = Style();
toggledStyle = const Style();
}
onSelectionChanged?.call(textSelection);
}
@ -426,5 +430,5 @@ class QuillController extends ChangeNotifier {
}
// Notify toolbar buttons directly with attributes
Map<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
/// paragraphs.
@immutable
class DefaultTextBlockStyle {
DefaultTextBlockStyle(
const DefaultTextBlockStyle(
this.style,
this.verticalSpacing,
this.lineSpacing,
@ -124,8 +125,9 @@ class InlineCodeStyle {
Object.hash(style, header1, header2, header3, backgroundColor, radius);
}
@immutable
class DefaultListBlockStyle extends DefaultTextBlockStyle {
DefaultListBlockStyle(
const DefaultListBlockStyle(
TextStyle style,
VerticalSpacing verticalSpacing,
VerticalSpacing lineSpacing,
@ -136,8 +138,9 @@ class DefaultListBlockStyle extends DefaultTextBlockStyle {
final QuillCheckboxBuilder? checkboxUIBuilder;
}
@immutable
class DefaultStyles {
DefaultStyles({
const DefaultStyles({
this.h1,
this.h2,
this.h3,

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

@ -533,9 +533,14 @@ class RawEditorState extends EditorState
? const BoxConstraints.expand()
: BoxConstraints(
minHeight: widget.minHeight ?? 0.0,
maxHeight: widget.maxHeight ?? double.infinity);
maxHeight: widget.maxHeight ?? double.infinity,
);
final isMacOS = Theme.of(context).platform == TargetPlatform.macOS;
// Please notice that this change will make the check fixed
// so if we ovveride the platform in material app theme data
// it will not depend on it and doesn't change here but I don't think
// we need to
final isDesktopMacOS = isMacOS();
return TextFieldTapRegion(
enabled: widget.enableUnfocusOnTapOutside,
@ -550,125 +555,125 @@ class RawEditorState extends EditorState
): const HideSelectionToolbarIntent(),
SingleActivator(
LogicalKeyboardKey.keyZ,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const UndoTextIntent(SelectionChangedCause.keyboard),
SingleActivator(
LogicalKeyboardKey.keyY,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const RedoTextIntent(SelectionChangedCause.keyboard),
// Selection formatting.
SingleActivator(
LogicalKeyboardKey.keyB,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.bold),
SingleActivator(
LogicalKeyboardKey.keyU,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.underline),
SingleActivator(
LogicalKeyboardKey.keyI,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.italic),
SingleActivator(
LogicalKeyboardKey.keyS,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ToggleTextStyleIntent(Attribute.strikeThrough),
SingleActivator(
LogicalKeyboardKey.backquote,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ToggleTextStyleIntent(Attribute.inlineCode),
SingleActivator(
LogicalKeyboardKey.tilde,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ToggleTextStyleIntent(Attribute.codeBlock),
SingleActivator(
LogicalKeyboardKey.keyB,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ToggleTextStyleIntent(Attribute.blockQuote),
SingleActivator(
LogicalKeyboardKey.keyK,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ApplyLinkIntent(),
// Lists
SingleActivator(
LogicalKeyboardKey.keyL,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ToggleTextStyleIntent(Attribute.ul),
SingleActivator(
LogicalKeyboardKey.keyO,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ToggleTextStyleIntent(Attribute.ol),
SingleActivator(
LogicalKeyboardKey.keyC,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const ApplyCheckListIntent(),
// Indents
SingleActivator(
LogicalKeyboardKey.keyM,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const IndentSelectionIntent(true),
SingleActivator(
LogicalKeyboardKey.keyM,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
shift: true,
): const IndentSelectionIntent(false),
// Headers
SingleActivator(
LogicalKeyboardKey.digit1,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h1),
SingleActivator(
LogicalKeyboardKey.digit2,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h2),
SingleActivator(
LogicalKeyboardKey.digit3,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.h3),
SingleActivator(
LogicalKeyboardKey.digit0,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const ApplyHeaderIntent(Attribute.header),
SingleActivator(
LogicalKeyboardKey.keyG,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const InsertEmbedIntent(Attribute.image),
SingleActivator(
LogicalKeyboardKey.keyF,
control: !isMacOS,
meta: isMacOS,
control: !isDesktopMacOS,
meta: isDesktopMacOS,
): const OpenSearchIntent(),
}, {
...?widget.customShortcuts
@ -915,31 +920,36 @@ class RawEditorState extends EditorState
textDirection: getDirectionOfNode(node), child: editableTextLine));
} else if (node is Block) {
final editableTextBlock = EditableTextBlock(
block: node,
controller: controller,
block: node,
controller: controller,
textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
textSelection: controller.selection,
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes,
);
result.add(
Directionality(
textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
textSelection: controller.selection,
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes);
result.add(Directionality(
textDirection: getDirectionOfNode(node), child: editableTextBlock));
child: editableTextBlock,
),
);
clearIndents = false;
} else {
@ -1111,9 +1121,15 @@ class RawEditorState extends EditorState
_styles = _styles!.merge(widget.customStyles!);
}
// TODO: this might need some attention
_requestFocusIfShould();
}
Future<void> _requestFocusIfShould() async {
if (!_didAutoFocus && widget.autoFocus) {
FocusScope.of(context).autofocus(widget.focusNode);
_didAutoFocus = true;
await Future.delayed(Duration.zero);
FocusScope.of(context).autofocus(widget.focusNode);
}
}
@ -1497,13 +1513,23 @@ class RawEditorState extends EditorState
final index = textEditingValue.selection.baseOffset;
final length = textEditingValue.selection.extentOffset - index;
final copied = controller.copiedImageUrl!;
controller.replaceText(index, length, BlockEmbed.image(copied.url), null);
controller.replaceText(
index,
length,
BlockEmbed.image(copied.url),
null,
);
if (copied.styleString.isNotEmpty) {
controller.formatText(getEmbedNode(controller, index + 1).offset, 1,
StyleAttribute(copied.styleString));
controller.formatText(
getEmbedNode(controller, index + 1).offset,
1,
StyleAttribute(copied.styleString),
);
}
controller.copiedImageUrl = null;
await Clipboard.setData(const ClipboardData(text: ''));
await Clipboard.setData(
const ClipboardData(text: ''),
);
return;
}
@ -1516,7 +1542,13 @@ class RawEditorState extends EditorState
final text = await Clipboard.getData(Clipboard.kTextPlain);
if (text != null) {
_replaceText(
ReplaceTextIntent(textEditingValue, text.text!, selection, cause));
ReplaceTextIntent(
textEditingValue,
text.text!,
selection,
cause,
),
);
bringIntoView(textEditingValue.selection.extent);
@ -1524,8 +1556,9 @@ class RawEditorState extends EditorState
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection:
TextSelection.collapsed(offset: textEditingValue.selection.end),
selection: TextSelection.collapsed(
offset: textEditingValue.selection.end,
),
),
cause,
);
@ -1533,14 +1566,15 @@ class RawEditorState extends EditorState
return;
}
if (widget.onImagePaste != null) {
final onImagePaste = widget.onImagePaste;
if (onImagePaste != null) {
final image = await Pasteboard.image;
if (image == null) {
return;
}
final imageUrl = await widget.onImagePaste!(image);
final imageUrl = await onImagePaste(image);
if (imageUrl == null) {
return;
}
@ -2559,8 +2593,13 @@ class _OpenSearchAction extends ContextAction<OpenSearchIntent> {
@override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
if (context == null) {
throw ArgumentError(
'The context should not be null to use invoke() method',
);
}
await showDialog<String>(
context: context!,
context: context,
builder: (_) => SearchDialog(controller: state.controller, text: ''),
);
}

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

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

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

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

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

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

Loading…
Cancel
Save