Improvemenets and allow to change the default text selection theme

pull/1437/head
Ahmed Hnewa 2 years ago
parent db143c9556
commit 4470a75def
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 4
      example/android/app/build.gradle
  2. 24
      example/lib/pages/home_page.dart
  3. 71
      flutter_quill_extensions/lib/embeds/builders.dart
  4. 6
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  5. 54
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  6. 91
      flutter_quill_extensions/lib/embeds/toolbar/media_button.dart
  7. 27
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  8. 2
      flutter_quill_extensions/lib/utils/quill_utils.dart
  9. 4
      flutter_quill_extensions/pubspec.yaml
  10. 2
      lib/src/models/documents/nodes/line.dart
  11. 4
      lib/src/models/documents/nodes/node.dart
  12. 8
      lib/src/models/documents/style.dart
  13. 101
      lib/src/translations/toolbar.i18n.dart
  14. 6
      lib/src/utils/platform.dart
  15. 6
      lib/src/widgets/controller.dart
  16. 9
      lib/src/widgets/default_styles.dart
  17. 13
      lib/src/widgets/editor.dart
  18. 74
      lib/src/widgets/raw_editor.dart
  19. 76
      lib/src/widgets/text_block.dart
  20. 84
      lib/src/widgets/toolbar/link_style_button.dart
  21. 3
      lib/src/widgets/toolbar/quill_icon_button.dart
  22. 16
      lib/src/widgets/toolbar/search_dialog.dart
  23. 34
      test/widgets/controller_test.dart

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

@ -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,
),
),
),
),
);

@ -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,9 @@ dependencies:
flutter:
sdk: flutter
flutter_quill: ^7.4.13
# flutter_quill: ^7.4.14
flutter_quill:
path: /Users/ahmedhnewa/development/playground/framework_based/flutter/flutter-quill
http: ^1.1.0
image_picker: ">=1.0.4"

@ -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) {

@ -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;
@ -405,7 +405,7 @@ class QuillController extends ChangeNotifier {
);
toggledStyle = style.removeAll(ignoredStyles.toSet());
} else {
toggledStyle = Style();
toggledStyle = const Style();
}
onSelectionChanged?.call(textSelection);
}
@ -426,5 +426,5 @@ class QuillController extends ChangeNotifier {
}
// Notify toolbar buttons directly with attributes
Map<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;

@ -535,7 +535,11 @@ class RawEditorState extends EditorState
minHeight: widget.minHeight ?? 0.0,
maxHeight: widget.maxHeight ?? double.infinity);
final isMacOS = Theme.of(context).platform == TargetPlatform.macOS;
// Please notice that this change will make the check fixed
// so if we ovveride the platform in material app theme data
// it will not depend on it and doesn't change here but I don't think
// we need to
final isMacOS = isAppleOS();
return TextFieldTapRegion(
enabled: widget.enableUnfocusOnTapOutside,
@ -915,31 +919,36 @@ class RawEditorState extends EditorState
textDirection: getDirectionOfNode(node), child: editableTextLine));
} else if (node is Block) {
final editableTextBlock = EditableTextBlock(
block: node,
controller: controller,
block: node,
controller: controller,
textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
textSelection: controller.selection,
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes,
);
result.add(
Directionality(
textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
textSelection: controller.selection,
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes);
result.add(Directionality(
textDirection: getDirectionOfNode(node), child: editableTextBlock));
child: editableTextBlock,
),
);
clearIndents = false;
} else {
@ -1111,9 +1120,15 @@ class RawEditorState extends EditorState
_styles = _styles!.merge(widget.customStyles!);
}
// TODO: this might need some attention
_requestFocusIfShould();
}
Future<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);
}
}
@ -2559,8 +2574,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);
}

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../models/documents/attribute.dart';
import '../../models/rules/insert.dart';
@ -189,46 +190,77 @@ class _LinkDialogState extends State<_LinkDialog> {
_textController = TextEditingController(text: _text);
}
@override
void dispose() {
_linkController.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
TextField(
keyboardType: TextInputType.multiline,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
content: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
TextFormField(
keyboardType: TextInputType.text,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Text'.i18n,
hintText: 'Please enter a text for your link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle),
autofocus: true,
onChanged: _textChanged,
controller: _textController,
),
const SizedBox(height: 16),
TextField(
keyboardType: TextInputType.multiline,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _textChanged,
controller: _textController,
textInputAction: TextInputAction.next,
autofillHints: [
AutofillHints.name,
AutofillHints.url,
],
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.url,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Link'.i18n,
hintText: 'Please enter the link url'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle),
autofocus: true,
onChanged: _linkChanged,
controller: _linkController,
),
],
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
onChanged: _linkChanged,
controller: _linkController,
textInputAction: TextInputAction.done,
autofillHints: [AutofillHints.url],
autocorrect: false,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
],
),
),
actions: [_okButton()],
actions: [
_okButton(),
],
);
}
Widget _okButton() {
if (widget.action != null) {
return widget.action!.builder(_canPress(), _applyLink);
return widget.action!.builder(
_canPress(),
_applyLink,
);
}
return TextButton(

@ -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