Merge remote-tracking branch 'origin/master'

pull/2125/head
AtlasAutocode 8 months ago
commit 434a24f3c0
  1. 8
      CHANGELOG.md
  2. 2
      CHANGELOG_DATA.json
  3. 2
      README.md
  4. 8
      dart_quill_delta/CHANGELOG.md
  5. 2
      dart_quill_delta/pubspec.yaml
  6. 6
      example/lib/screens/quill/my_quill_editor.dart
  7. 14
      example/macos/Podfile.lock
  8. 2
      example/macos/Runner/AppDelegate.swift
  9. 8
      flutter_quill_extensions/CHANGELOG.md
  10. 12
      flutter_quill_extensions/lib/embeds/embed_types.dart
  11. 277
      flutter_quill_extensions/lib/embeds/others/image_video_utils.dart
  12. 537
      flutter_quill_extensions/lib/embeds/others/media_button/media_button.dart
  13. 19
      flutter_quill_extensions/lib/embeds/unknown/editor/unknown_embed.dart
  14. 64
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  15. 71
      flutter_quill_extensions/lib/models/config/media/media_button_configurations.dart
  16. 0
      flutter_quill_extensions/lib/src/common/extensions/attribute.dart
  17. 0
      flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart
  18. 122
      flutter_quill_extensions/lib/src/common/image_video_utils.dart
  19. 0
      flutter_quill_extensions/lib/src/common/utils/dart_ui/dart_ui_fake.dart
  20. 0
      flutter_quill_extensions/lib/src/common/utils/dart_ui/dart_ui_real.dart
  21. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_shared_utils.dart
  22. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_utils.dart
  23. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_web_utils.dart
  24. 0
      flutter_quill_extensions/lib/src/common/utils/patterns.dart
  25. 0
      flutter_quill_extensions/lib/src/common/utils/quill_image_utils.dart
  26. 0
      flutter_quill_extensions/lib/src/common/utils/quill_table_utils.dart
  27. 0
      flutter_quill_extensions/lib/src/common/utils/string.dart
  28. 4
      flutter_quill_extensions/lib/src/common/utils/utils.dart
  29. 0
      flutter_quill_extensions/lib/src/editor/formula/formula_embed.dart
  30. 8
      flutter_quill_extensions/lib/src/editor/image/image_embed.dart
  31. 4
      flutter_quill_extensions/lib/src/editor/image/image_embed_types.dart
  32. 16
      flutter_quill_extensions/lib/src/editor/image/image_menu.dart
  33. 10
      flutter_quill_extensions/lib/src/editor/image/image_web_embed.dart
  34. 2
      flutter_quill_extensions/lib/src/editor/image/models/image_configurations.dart
  35. 0
      flutter_quill_extensions/lib/src/editor/image/models/image_web_configurations.dart
  36. 6
      flutter_quill_extensions/lib/src/editor/image/widgets/image.dart
  37. 0
      flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart
  38. 0
      flutter_quill_extensions/lib/src/editor/table/table_cell_embed.dart
  39. 2
      flutter_quill_extensions/lib/src/editor/table/table_embed.dart
  40. 0
      flutter_quill_extensions/lib/src/editor/table/table_models.dart
  41. 0
      flutter_quill_extensions/lib/src/editor/video/models/video_configurations.dart
  42. 0
      flutter_quill_extensions/lib/src/editor/video/models/video_web_configurations.dart
  43. 0
      flutter_quill_extensions/lib/src/editor/video/models/youtube_video_support_mode.dart
  44. 10
      flutter_quill_extensions/lib/src/editor/video/video_embed.dart
  45. 10
      flutter_quill_extensions/lib/src/editor/video/video_web_embed.dart
  46. 2
      flutter_quill_extensions/lib/src/editor/video/widgets/video_app.dart
  47. 2
      flutter_quill_extensions/lib/src/editor/video/widgets/youtube_video_app.dart
  48. 0
      flutter_quill_extensions/lib/src/editor_toolbar_controller_shared/clipboard/super_clipboard_service.dart
  49. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/image_options.dart
  50. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/image_picker.dart
  51. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/packages/image_picker.dart
  52. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/s_image_picker.dart
  53. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/exceptions.dart
  54. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/image_saver.dart
  55. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/packages/gal.dart
  56. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/s_image_saver.dart
  57. 4
      flutter_quill_extensions/lib/src/editor_toolbar_shared/shared_configurations.dart
  58. 36
      flutter_quill_extensions/lib/src/flutter_quill_embeds.dart
  59. 6
      flutter_quill_extensions/lib/src/toolbar/camera/camera_button.dart
  60. 4
      flutter_quill_extensions/lib/src/toolbar/camera/camera_types.dart
  61. 2
      flutter_quill_extensions/lib/src/toolbar/camera/models/camera_configurations.dart
  62. 0
      flutter_quill_extensions/lib/src/toolbar/camera/select_camera_action.dart
  63. 2
      flutter_quill_extensions/lib/src/toolbar/formula/formula_button.dart
  64. 0
      flutter_quill_extensions/lib/src/toolbar/formula/models/formula_configurations.dart
  65. 10
      flutter_quill_extensions/lib/src/toolbar/image/image_button.dart
  66. 2
      flutter_quill_extensions/lib/src/toolbar/image/models/image_configurations.dart
  67. 2
      flutter_quill_extensions/lib/src/toolbar/image/select_image_source.dart
  68. 0
      flutter_quill_extensions/lib/src/toolbar/table/models/table_configurations.dart
  69. 5
      flutter_quill_extensions/lib/src/toolbar/table/table_button.dart
  70. 4
      flutter_quill_extensions/lib/src/toolbar/video/models/video.dart
  71. 2
      flutter_quill_extensions/lib/src/toolbar/video/models/video_configurations.dart
  72. 2
      flutter_quill_extensions/lib/src/toolbar/video/select_video_source.dart
  73. 10
      flutter_quill_extensions/lib/src/toolbar/video/video_button.dart
  74. 2
      flutter_quill_extensions/pubspec.yaml
  75. 8
      flutter_quill_test/CHANGELOG.md
  76. 2
      flutter_quill_test/pubspec.yaml
  77. 21
      lib/src/document/document.dart
  78. 27
      lib/src/document/nodes/line.dart
  79. 19
      lib/src/editor/editor.dart
  80. 13
      lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart
  81. 21
      lib/src/rules/insert.dart
  82. 2
      pubspec.yaml
  83. 48
      test/document/document_test.dart
  84. 83
      test/document/line_test.dart

@ -4,6 +4,14 @@
All notable changes to this project will be documented in this file.
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

File diff suppressed because one or more lines are too long

@ -136,7 +136,7 @@ and attach the `QuillController` to them:
```dart
QuillSimpleToolbar(
controller: _controller,
configurations: QuillSimpleToolbarConfigurations(),
configurations: const QuillSimpleToolbarConfigurations(),
),
Expanded(
child: QuillEditor.basic(

@ -4,6 +4,14 @@
All notable changes to this project will be documented in this file.
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,6 +1,6 @@
name: dart_quill_delta
description: A port of quill-js-delta from typescript to dart
version: 10.1.10
version: 10.2.0
homepage: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/
repository: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart'
show isAndroid, isDesktop, isIOS, isWeb;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/embeds/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:flutter_quill_extensions/models/config/video/editor/youtube_video_support_mode.dart';
// ignore: implementation_imports
import 'package:flutter_quill_extensions/src/editor/image/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:path/path.dart' as path;
import '../../extensions/scaffold_messenger.dart';

@ -18,8 +18,6 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- printing (1.0.0):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- sqflite (0.0.3):
@ -42,7 +40,6 @@ DEPENDENCIES:
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
@ -70,8 +67,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
printing:
:path: Flutter/ephemeral/.symlinks/plugins/printing/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
sqflite:
@ -86,19 +81,18 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
PODFILE CHECKSUM: 7159dd71cf9f57a5669bb2dee7a5030dbcc0483f

@ -1,7 +1,7 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true

@ -4,6 +4,14 @@
All notable changes to this project will be documented in this file.
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,12 +0,0 @@
import 'package:cross_file/cross_file.dart' show XFile;
typedef MediaFileUrl = String;
typedef MediaFilePicker = Future<XFile?> Function(QuillMediaType mediaType);
typedef MediaPickedCallback = Future<MediaFileUrl> Function(XFile file);
enum QuillMediaType { image, video }
extension QuillMediaTypeX on QuillMediaType {
bool get isImage => this == QuillMediaType.image;
bool get isVideo => this == QuillMediaType.video;
}

@ -1,277 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
import 'package:flutter_quill/translations.dart';
import '../../utils/patterns.dart';
enum LinkType {
video,
image,
}
class TypeLinkDialog extends StatefulWidget {
const TypeLinkDialog({
required this.linkType,
this.dialogTheme,
this.link,
this.linkRegExp,
super.key,
});
final QuillDialogTheme? dialogTheme;
final String? link;
final RegExp? linkRegExp;
final LinkType linkType;
@override
TypeLinkDialogState createState() => TypeLinkDialogState();
}
class TypeLinkDialogState extends State<TypeLinkDialog> {
late String _link;
late TextEditingController _controller;
RegExp? _linkRegExp;
@override
void initState() {
super.initState();
_link = widget.link ?? '';
_controller = TextEditingController(text: _link);
_linkRegExp = widget.linkRegExp;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField(
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: context.loc.pasteLink,
hintText: widget.linkType == LinkType.image
? context.loc.pleaseEnterAValidImageURL
: context.loc.pleaseEnterAValidVideoURL,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _linkChanged,
controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
actions: [
TextButton(
onPressed: _canPress() ? _applyLink : null,
child: Text(
context.loc.ok,
style: widget.dialogTheme?.labelTextStyle,
),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link.trim());
}
RegExp get linkRegExp {
final customRegExp = _linkRegExp;
if (customRegExp != null) {
return customRegExp;
}
switch (widget.linkType) {
case LinkType.video:
if (youtubeRegExp.hasMatch(_link)) {
return youtubeRegExp;
}
return videoRegExp;
case LinkType.image:
return imageRegExp;
}
}
bool _canPress() {
if (_link.isEmpty) {
return false;
}
if (widget.linkType == LinkType.image) {}
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
}
}
// @immutable
// class ImageVideoUtils {
// const ImageVideoUtils._();
// static Future<MediaPickSetting?> selectMediaPickSetting(
// BuildContext context,
// ) =>
// showDialog<MediaPickSetting>(
// context: context,
// builder: (ctx) => AlertDialog(
// contentPadding: EdgeInsets.zero,
// content: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// TextButton.icon(
// icon: const Icon(
// Icons.collections,
// color: Colors.orangeAccent,
// ),
// label: Text('Gallery'.i18n),
// onPressed: () => Navigator.pop(ctx,
// MediaPickSetting.gallery),
// ),
// TextButton.icon(
// icon: const Icon(
// Icons.link,
// color: Colors.cyanAccent,
// ),
// label: Text('Link'.i18n),
// onPressed: () => Navigator.pop(ctx, MediaPickSetting.link),
// )
// ],
// ),
// ),
// );
// /// For image picking logic
// static Future<void> handleImageButtonTap(
// BuildContext context,
// QuillController controller,
// ImageSource imageSource,
// OnImagePickCallback onImagePickCallback, {
// FilePickImpl? filePickImpl,
// WebImagePickImpl? webImagePickImpl,
// }) async {
// String? imageUrl;
// if (kIsWeb) {
// if (webImagePickImpl != null) {
// imageUrl = await webImagePickImpl(onImagePickCallback);
// return;
// }
// final file = await ImagePicker()
//.pickImage(source: ImageSource.gallery);
// imageUrl = file?.path;
// if (imageUrl == null) {
// return;
// }
// } else if (isMobile()) {
// imageUrl = await _pickImage(imageSource, onImagePickCallback);
// } else {
// assert(filePickImpl != null, 'Desktop must provide filePickImpl');
// imageUrl =
// await _pickImageDesktop
//(context, filePickImpl!, onImagePickCallback);
// }
// if (imageUrl == null) {
// return;
// }
// controller.insertImageBlock(
// imageUrl: imageUrl,
// );
// }
// static Future<String?> _pickImage(
// ImageSource source,
// OnImagePickCallback onImagePickCallback,
// ) async {
// final pickedFile = await ImagePicker().pickImage(source: source);
// if (pickedFile == null) {
// return null;
// }
// return onImagePickCallback(File(pickedFile.path));
// }
// static Future<String?> _pickImageDesktop(
// BuildContext context,
// FilePickImpl filePickImpl,
// OnImagePickCallback onImagePickCallback,
// ) async {
// final filePath = await filePickImpl(context);
// if (filePath == null || filePath.isEmpty) return null;
// final file = File(filePath);
// return onImagePickCallback(file);
// }
// /// For video picking logic
// static Future<void> handleVideoButtonTap(
// BuildContext context,
// QuillController controller,
// ImageSource videoSource,
// OnVideoPickCallback onVideoPickCallback, {
// FilePickImpl? filePickImpl,
// WebVideoPickImpl? webVideoPickImpl,
// }) async {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// String? videoUrl;
// if (kIsWeb) {
// assert(
// webVideoPickImpl != null,
// 'Please provide webVideoPickImpl for Web '
// 'in the options of this button',
// );
// videoUrl = await webVideoPickImpl!(onVideoPickCallback);
// } else if (isMobile()) {
// videoUrl = await _pickVideo(videoSource, onVideoPickCallback);
// } else {
// assert(filePickImpl != null, 'Desktop must provide filePickImpl');
// videoUrl =
// await _pickVideoDesktop(context, filePickImpl!,
// onVideoPickCallback);
// }
// if (videoUrl != null) {
// controller.replaceText(index, length, BlockEmbed.video(videoUrl),
// null);
// }
// }
// static Future<String?> _pickVideo(
// ImageSource source, OnVideoPickCallback onVideoPickCallback) async {
// final pickedFile = await ImagePicker().pickVideo(source: source);
// if (pickedFile == null) {
// return null;
// }
// return onVideoPickCallback(File(pickedFile.path));
// }
// static Future<String?> _pickVideoDesktop(
// BuildContext context,
// FilePickImpl filePickImpl,
// OnVideoPickCallback onVideoPickCallback) async {
// final filePath = await filePickImpl(context);
// if (filePath == null || filePath.isEmpty) return null;
// final file = File(filePath);
// return onVideoPickCallback(file);
// }
// }

@ -1,537 +0,0 @@
// // ignore_for_file: use_build_context_synchronously
// import 'dart:math' as math;
// import 'dart:ui';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_quill/extensions.dart';
// import 'package:flutter_quill/flutter_quill.dart';
// import 'package:flutter_quill/translations.dart';
// import 'package:image_picker/image_picker.dart';
// import '../../../models/config/toolbar/buttons/media_button.dart';
// import '../../embed_types.dart';
// import '../utils/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
// /// on [http://quilljs.com].
// class QuillToolbarMediaButton extends StatelessWidget {
// QuillToolbarMediaButton({
// required this.controller,
// this.options,
// super.key,
// }) : assert(options.type == QuillMediaType.image,
// 'Video selection is not supported yet');
// final QuillController controller;
// final QuillToolbarMediaButtonOptions options;
// double _iconSize(BuildContext context) {
// final baseFontSize = baseButtonExtraOptions(context).globalIconSize;
// final iconSize = options.iconSize;
// return iconSize ?? baseFontSize;
// }
// VoidCallback? _afterButtonPressed(BuildContext context) {
// return options.afterButtonPressed ??
// baseButtonExtraOptions(context).afterButtonPressed;
// }
// QuillIconTheme? _iconTheme(BuildContext context) {
// return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme;
// }
// QuillToolbarBaseButtonOptions baseButtonExtraOptions(
//BuildContext context) {
// return context.requireQuillToolbarBaseButtonOptions;
// }
// (IconData, String) get _defaultData {
// switch (options.type) {
// case QuillMediaType.image:
// return (Icons.perm_media, 'Photo media button');
// case QuillMediaType.video:
// throw UnsupportedError('The video is not supported yet.');
// }
// }
// IconData _iconData(BuildContext context) {
// return options.iconData ??
// baseButtonExtraOptions(context).iconData ??
// _defaultData.$1;
// }
// String _tooltip(BuildContext context) {
// return options.tooltip ??
// baseButtonExtraOptions(context).tooltip ??
// _defaultData.$2;
// // ('Camera'.i18n);
// }
// void _sharedOnPressed(BuildContext context) {
// _onPressedHandler(context);
// _afterButtonPressed(context);
// }
// @override
// Widget build(BuildContext context) {
// final tooltip = _tooltip(context);
// final iconSize = _iconSize(context);
// final iconData = _iconData(context);
// final childBuilder =
// options.childBuilder ?? baseButtonExtraOptions(context).childBuilder;
// final iconTheme = _iconTheme(context);
// if (childBuilder != null) {
// return childBuilder(
// QuillToolbarMediaButtonOptions(
// type: options.type,
// onMediaPickedCallback: options.onMediaPickedCallback,
// onImagePickCallback: options.onImagePickCallback,
// onVideoPickCallback: options.onVideoPickCallback,
// iconData: iconData,
// afterButtonPressed: _afterButtonPressed(context),
// autovalidateMode: options.autovalidateMode,
// childrenSpacing: options.childrenSpacing,
// dialogBarrierColor: options.dialogBarrierColor,
// dialogTheme: options.dialogTheme,
// filePickImpl: options.filePickImpl,
// fillColor: options.fillColor,
// galleryButtonText: options.galleryButtonText,
// iconTheme: iconTheme,
// iconSize: iconSize,
// iconButtonFactor: iconButtonFactor,
// hintText: options.hintText,
// labelText: options.labelText,
// submitButtonSize: options.submitButtonSize,
// linkButtonText: options.linkButtonText,
// mediaFilePicker: options.mediaFilePicker,
// submitButtonText: options.submitButtonText,
// validationMessage: options.validationMessage,
// webImagePickImpl: options.webImagePickImpl,
// webVideoPickImpl: options.webVideoPickImpl,
// tooltip: options.tooltip,
// ),
// QuillToolbarMediaButtonExtraOptions(
// context: context,
// controller: controller,
// onPressed: () => _sharedOnPressed(context),
// ),
// );
// }
// final theme = Theme.of(context);
// final iconColor =
// options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color;
// final iconFillColor = options.iconTheme?.iconUnselectedFillColor ??
// options.fillColor ??
// theme.canvasColor;
// return QuillToolbarIconButton(
// icon: Icon(iconData, size: iconSize, color: iconColor),
// tooltip: tooltip,
// highlightElevation: 0,
// hoverElevation: 0,
// size: iconSize * iconButtonFactor,
// fillColor: iconFillColor,
// borderRadius: iconTheme?.borderRadius ?? 2,
// onPressed: () => _sharedOnPressed(context),
// );
// }
// Future<void> _onPressedHandler(BuildContext context) async {
// if (options.onMediaPickedCallback == null) {
// _inputLink(context);
// return;
// }
// final mediaSource = await showDialog<MediaPickSetting>(
// context: context,
// builder: (_) => MediaSourceSelectorDialog(
// dialogTheme: options.dialogTheme,
// galleryButtonText: options.galleryButtonText,
// linkButtonText: options.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,
// options.onImagePickCallback,
// filePickImpl: options.filePickImpl,
// webImagePickImpl: options.webImagePickImpl,
// );
// break;
// case MediaPickSetting.video:
// await ImageVideoUtils.handleVideoButtonTap(
// context,
// controller,
// ImageSource.camera,
// options.onVideoPickCallback,
// filePickImpl: options.filePickImpl,
// webVideoPickImpl: options.webVideoPickImpl,
// );
// break;
// }
// }
// Future<void> _pickImage() async {
// if (!(kIsWeb || isMobile() || isDesktop())) {
// throw UnsupportedError(
// 'Unsupported target platform: ${defaultTargetPlatform.name}',
// );
// }
// final mediaFileUrl = await _pickMediaFileUrl();
// if (mediaFileUrl != null) {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// controller.replaceText(
// index,
// length,
// BlockEmbed.image(mediaFileUrl),
// null,
// );
// }
// }
// Future<MediaFileUrl?> _pickMediaFileUrl() async {
// final mediaFile = await options.mediaFilePicker?.call(options.type);
// return mediaFile != null
// ? options.onMediaPickedCallback?.call(mediaFile)
// : null;
// }
// void _inputLink(BuildContext context) {
// showDialog<String>(
// context: context,
// builder: (_) => MediaLinkDialog(
// dialogTheme: options.dialogTheme,
// labelText: options.labelText,
// hintText: options.hintText,
// buttonText: options.submitButtonText,
// buttonSize: options.submitButtonSize,
// childrenSpacing: options.childrenSpacing,
// autovalidateMode: options.autovalidateMode,
// validationMessage: options.validationMessage,
// ),
// ).then(_linkSubmitted);
// }
// void _linkSubmitted(String? value) {
// if (value != null && value.isNotEmpty) {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// final data = options.type.isImage
// ? BlockEmbed.image(value)
// : BlockEmbed.video(value);
// controller.replaceText(index, length, data, null);
// }
// }
// }
// /// Provides a dialog for input link to media resource.
// class MediaLinkDialog extends StatefulWidget {
// const MediaLinkDialog({
// super.key,
// this.link,
// this.dialogTheme,
// this.childrenSpacing = 16.0,
// this.labelText,
// this.hintText,
// this.buttonText,
// this.buttonSize,
// this.autovalidateMode = AutovalidateMode.disabled,
// this.validationMessage,
// }) : assert(childrenSpacing > 0);
// final String? link;
// final QuillDialogTheme? dialogTheme;
// /// The margin between child widgets in the dialog.
// final double childrenSpacing;
// /// The text of label in link add mode.
// final String? labelText;
// /// The hint text for link [TextField].
// final String? hintText;
// /// The text of the submit button.
// final String? buttonText;
// /// The size of dialog buttons.
// final Size? buttonSize;
// final AutovalidateMode autovalidateMode;
// final String? validationMessage;
// @override
// State<MediaLinkDialog> createState() => _MediaLinkDialogState();
// }
// class _MediaLinkDialogState extends State<MediaLinkDialog> {
// final _linkFocus = FocusNode();
// final _linkController = TextEditingController();
// @override
// void dispose() {
// _linkFocus.dispose();
// _linkController.dispose();
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// final constraints = widget.dialogTheme?.linkDialogConstraints ??
// () {
// final size = MediaQuery.sizeOf(context);
// final maxWidth = kIsWeb ? size.width / 4 : size.width - 80;
// return BoxConstraints(maxWidth: maxWidth, maxHeight: 80);
// }();
// final buttonStyle = widget.buttonSize != null
// ? Theme.of(context)
// .elevatedButtonTheme
// .style
// ?.copyWith(
//fixedSize: MaterialStatePropertyAll(widget.buttonSize))
// : widget.dialogTheme?.buttonStyle;
// final isWrappable = widget.dialogTheme?.isWrappable ?? false;
// final children = [
// Text(widget.labelText ?? 'Enter media'.i18n),
// UtilityWidgets.maybeWidget(
// enabled: !isWrappable,
// wrapper: (child) => Expanded(
// child: child,
// ),
// child: Padding(
// padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing),
// child: TextFormField(
// controller: _linkController,
// focusNode: _linkFocus,
// style: widget.dialogTheme?.inputTextStyle,
// keyboardType: TextInputType.url,
// textInputAction: TextInputAction.done,
// decoration: InputDecoration(
// labelStyle: widget.dialogTheme?.labelTextStyle,
// hintText: widget.hintText,
// ),
// autofocus: true,
// autovalidateMode: widget.autovalidateMode,
// validator: _validateLink,
// onChanged: _linkChanged,
// ),
// ),
// ),
// ElevatedButton(
// onPressed: _canPress() ? _submitLink : null,
// style: buttonStyle,
// child: Text(widget.buttonText ?? 'Ok'.i18n),
// ),
// ];
// return Dialog(
// backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
// shape: widget.dialogTheme?.shape ??
// DialogTheme.of(context).shape ??
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
// child: ConstrainedBox(
// constraints: constraints,
// child: Padding(
// padding:
// widget.dialogTheme?.linkDialogPadding ?? const
// EdgeInsets.all(16),
// child: Form(
// child: isWrappable
// ? Wrap(
// alignment: WrapAlignment.center,
// crossAxisAlignment: WrapCrossAlignment.center,
// runSpacing: widget.dialogTheme?.runSpacing ?? 0.0,
// children: children,
// )
// : Row(
// children: children,
// ),
// ),
// ),
// ),
// );
// }
// bool _canPress() => _validateLink(_linkController.text) == null;
// void _linkChanged(String value) {
// setState(() {
// _linkController.text = value;
// });
// }
// void _submitLink() => Navigator.pop(context, _linkController.text);
// String? _validateLink(String? value) {
// if ((value?.isEmpty ?? false) ||
// !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) {
// return widget.validationMessage ?? 'That is not a valid URL';
// }
// return null;
// }
// }
// /// Media souce selector.
// class MediaSourceSelectorDialog extends StatelessWidget {
// const MediaSourceSelectorDialog({
// super.key,
// this.dialogTheme,
// this.galleryButtonText,
// this.linkButtonText,
// });
// final QuillDialogTheme? dialogTheme;
// /// The text of the gallery button [MediaSourceSelectorDialog].
// final String? galleryButtonText;
// /// The text of the link button [MediaSourceSelectorDialog].
// final String? linkButtonText;
// @override
// Widget build(BuildContext context) {
// final constraints = dialogTheme?.mediaSelectorDialogConstraints ??
// () {
// final size = MediaQuery.sizeOf(context);
// double maxWidth, maxHeight;
// if (kIsWeb) {
// maxWidth = size.width / 7;
// maxHeight = size.height / 7;
// } else {
// maxWidth = size.width - 80;
// maxHeight = maxWidth / 2;
// }
// return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight);
// }();
// final shape = dialogTheme?.shape ??
// DialogTheme.of(context).shape ??
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4));
// return Dialog(
// backgroundColor: dialogTheme?.dialogBackgroundColor,
// shape: shape,
// child: ConstrainedBox(
// constraints: constraints,
// child: Padding(
// padding: dialogTheme?.mediaSelectorDialogPadding ??
// const EdgeInsets.all(16),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: TextButtonWithIcon(
// icon: Icons.collections,
// label: galleryButtonText ?? 'Gallery'.i18n,
// onPressed: () =>
// Navigator.pop(context, MediaPickSetting.gallery),
// ),
// ),
// const SizedBox(width: 10),
// Expanded(
// child: TextButtonWithIcon(
// icon: Icons.link,
// label: linkButtonText ?? 'Link'.i18n,
// onPressed: () =>
// Navigator.pop(context, MediaPickSetting.link),
// ),
// )
// ],
// ),
// ),
// ),
// );
// }
// }
// class TextButtonWithIcon extends StatelessWidget {
// const TextButtonWithIcon({
// required this.label,
// required this.icon,
// required this.onPressed,
// this.textStyle,
// super.key,
// });
// final String label;
// final IconData icon;
// final VoidCallback onPressed;
// final TextStyle? textStyle;
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
// final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
// final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
// final buttonStyle = TextButtonTheme.of(context).style;
// final shape = buttonStyle?.shape?.resolve({}) ??
// const RoundedRectangleBorder(
// borderRadius: BorderRadius.all(
// Radius.circular(4),
// ),
// );
// return Material(
// shape: shape,
// textStyle: textStyle ??
// theme.textButtonTheme.style?.textStyle?.resolve({}) ??
// theme.textTheme.labelLarge,
// elevation: buttonStyle?.elevation?.resolve({}) ?? 0,
// child: InkWell(
// customBorder: shape,
// onTap: onPressed,
// child: Padding(
// padding: const EdgeInsets.all(16),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// Icon(icon),
// SizedBox(height: gap),
// Flexible(child: Text(label)),
// ],
// ),
// ),
// ),
// );
// }
// }
// /// Default file picker.
// // Future<QuillFile?> _defaultMediaPicker(QuillMediaType mediaType) async {
// // final pickedFile = mediaType.isImage
// // ? await ImagePicker().pickImage(source: ImageSource.gallery)
// // : await ImagePicker().pickVideo(source: ImageSource.gallery);
// // if (pickedFile != null) {
// // return QuillFile(
// // name: pickedFile.name,
// // path: pickedFile.path,
// // bytes: await pickedFile.readAsBytes(),
// // );
// // }
// // return null;
// // }

@ -1,19 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_quill/flutter_quill.dart';
class QuillEditorUnknownEmbedBuilder extends EmbedBuilder {
@override
Widget build(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
bool inline,
TextStyle textStyle,
) {
return const Text('Unknown embed builder');
}
@override
String get key => 'unknown';
}

@ -4,42 +4,36 @@ library flutter_quill_extensions;
import 'package:flutter_quill/src/editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
import 'package:meta/meta.dart' show immutable;
import 'services/clipboard/super_clipboard_service.dart';
import 'src/editor_toolbar_controller_shared/clipboard/super_clipboard_service.dart';
export 'embeds/embed_types.dart';
export 'embeds/formula/toolbar/formula_button.dart';
export 'embeds/image/editor/image_embed.dart';
export 'embeds/image/editor/image_embed_types.dart';
export 'embeds/image/editor/image_web_embed.dart';
export 'embeds/image/toolbar/image_button.dart';
export 'embeds/others/camera_button/camera_button.dart';
export 'embeds/others/media_button/media_button.dart';
export 'embeds/table/editor/table_cell_embed.dart';
export 'embeds/table/editor/table_embed.dart';
export 'embeds/table/editor/table_models.dart';
export 'embeds/table/toolbar/table_button.dart';
export 'embeds/unknown/editor/unknown_embed.dart';
export 'embeds/video/editor/video_embed.dart';
export 'embeds/video/editor/video_web_embed.dart';
export 'embeds/video/toolbar/video_button.dart';
export 'embeds/video/video.dart';
export 'extensions/controller_ext.dart';
export 'flutter_quill_embeds.dart';
export 'models/config/camera/camera_configurations.dart';
export 'models/config/formula/formula_configurations.dart';
export 'models/config/image/editor/image_configurations.dart';
export 'models/config/image/editor/image_web_configurations.dart';
export 'models/config/image/toolbar/image_configurations.dart';
export 'models/config/media/media_button_configurations.dart';
export 'models/config/shared_configurations.dart';
export 'models/config/table/table_configurations.dart';
export 'models/config/video/editor/video_configurations.dart';
export 'models/config/video/editor/video_web_configurations.dart';
export 'models/config/video/toolbar/video_configurations.dart';
export 'utils/utils.dart';
// TODO: Refactor flutter_quill_extensions to match the structure of flutter_quill
// Also avoid exposing all APIs as public. Use `src` as directory name
export 'src/common/extensions/controller_ext.dart';
export 'src/common/utils/utils.dart';
export 'src/editor/image/image_embed.dart';
export 'src/editor/image/image_embed_types.dart';
export 'src/editor/image/image_web_embed.dart';
export 'src/editor/image/models/image_configurations.dart';
export 'src/editor/image/models/image_web_configurations.dart';
export 'src/editor/table/table_cell_embed.dart';
export 'src/editor/table/table_embed.dart';
export 'src/editor/table/table_models.dart';
export 'src/editor/video/models/video_configurations.dart';
export 'src/editor/video/models/video_web_configurations.dart';
export 'src/editor/video/models/youtube_video_support_mode.dart';
export 'src/editor/video/video_embed.dart';
export 'src/editor/video/video_web_embed.dart';
export 'src/editor_toolbar_shared/shared_configurations.dart';
export 'src/flutter_quill_embeds.dart';
export 'src/toolbar/camera/camera_button.dart';
export 'src/toolbar/camera/models/camera_configurations.dart';
export 'src/toolbar/formula/formula_button.dart';
export 'src/toolbar/formula/models/formula_configurations.dart';
export 'src/toolbar/image/image_button.dart';
export 'src/toolbar/image/models/image_configurations.dart';
export 'src/toolbar/table/models/table_configurations.dart';
export 'src/toolbar/table/table_button.dart';
export 'src/toolbar/video/models/video.dart';
export 'src/toolbar/video/models/video_configurations.dart';
export 'src/toolbar/video/video_button.dart';
@immutable
class FlutterQuillExtensions {

@ -1,71 +0,0 @@
import 'package:flutter/widgets.dart' show AutovalidateMode;
import 'package:flutter/widgets.dart' show Color, Size;
import 'package:flutter_quill/flutter_quill.dart';
import '../../../embeds/embed_types.dart';
class QuillToolbarMediaButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {
const QuillToolbarMediaButtonExtraOptions({
required super.controller,
required super.context,
required super.onPressed,
});
}
class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions<
QuillToolbarMediaButtonOptions, QuillToolbarMediaButtonExtraOptions> {
const QuillToolbarMediaButtonOptions({
required this.type,
required this.onMediaPickedCallback,
// required this.onVideoPickCallback,
this.dialogBarrierColor,
this.mediaFilePicker,
this.childrenSpacing = 16.0,
this.autovalidateMode = AutovalidateMode.disabled,
super.iconSize,
this.dialogTheme,
this.labelText,
this.hintText,
this.submitButtonText,
this.submitButtonSize,
this.galleryButtonText,
this.linkButtonText,
this.validationMessage,
super.iconData,
super.afterButtonPressed,
super.tooltip,
super.iconTheme,
super.childBuilder,
});
final QuillMediaType type;
final QuillDialogTheme? dialogTheme;
final MediaFilePicker? mediaFilePicker;
final MediaPickedCallback? onMediaPickedCallback;
final Color? dialogBarrierColor;
/// The margin between child widgets in the dialog.
final double childrenSpacing;
/// The text of label in link add mode.
final String? labelText;
/// The hint text for link [TextField].
final String? hintText;
/// The text of the submit button.
final String? submitButtonText;
/// The size of dialog buttons.
final Size? submitButtonSize;
/// The text of the gallery button [MediaSourceSelectorDialog].
final String? galleryButtonText;
/// The text of the link button [MediaSourceSelectorDialog].
final String? linkButtonText;
final AutovalidateMode autovalidateMode;
final String? validationMessage;
}

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
import 'package:flutter_quill/translations.dart';
import 'utils/patterns.dart';
enum LinkType {
video,
image,
}
class TypeLinkDialog extends StatefulWidget {
const TypeLinkDialog({
required this.linkType,
this.dialogTheme,
this.link,
this.linkRegExp,
super.key,
});
final QuillDialogTheme? dialogTheme;
final String? link;
final RegExp? linkRegExp;
final LinkType linkType;
@override
TypeLinkDialogState createState() => TypeLinkDialogState();
}
class TypeLinkDialogState extends State<TypeLinkDialog> {
late String _link;
late TextEditingController _controller;
RegExp? _linkRegExp;
@override
void initState() {
super.initState();
_link = widget.link ?? '';
_controller = TextEditingController(text: _link);
_linkRegExp = widget.linkRegExp;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField(
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: context.loc.pasteLink,
hintText: widget.linkType == LinkType.image
? context.loc.pleaseEnterAValidImageURL
: context.loc.pleaseEnterAValidVideoURL,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _linkChanged,
controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
actions: [
TextButton(
onPressed: _canPress() ? _applyLink : null,
child: Text(
context.loc.ok,
style: widget.dialogTheme?.labelTextStyle,
),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link.trim());
}
RegExp get linkRegExp {
final customRegExp = _linkRegExp;
if (customRegExp != null) {
return customRegExp;
}
switch (widget.linkType) {
case LinkType.video:
if (youtubeRegExp.hasMatch(_link)) {
return youtubeRegExp;
}
return videoRegExp;
case LinkType.image:
return imageRegExp;
}
}
bool _canPress() {
if (_link.isEmpty) {
return false;
}
if (widget.linkType == LinkType.image) {}
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
}
}

@ -4,8 +4,8 @@ import 'package:cross_file/cross_file.dart';
import 'package:flutter/foundation.dart' show Uint8List, immutable;
import 'package:http/http.dart' as http;
import '../embeds/widgets/image.dart';
import '../services/image_saver/s_image_saver.dart';
import '../../editor/image/widgets/image.dart';
import '../../editor_toolbar_shared/image_saver/s_image_saver.dart';
import 'patterns.dart';
bool isBase64(String str) {

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize;
import 'package:flutter_quill/translations.dart';
import '../../../models/config/image/editor/image_configurations.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../widgets/image.dart';
import '../../common/utils/element_utils/element_utils.dart';
import '../../editor_toolbar_shared/shared_configurations.dart';
import 'image_menu.dart';
import 'models/image_configurations.dart';
import 'widgets/image.dart';
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
QuillEditorImageEmbedBuilder({

@ -4,8 +4,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../extensions/controller_ext.dart';
import '../../../services/image_picker/s_image_picker.dart';
import '../../common/extensions/controller_ext.dart';
import '../../editor_toolbar_shared/image_picker/s_image_picker.dart';
/// When request picking an image, for example when the image button toolbar
/// clicked, it should be null in case the user didn't choose any image or

@ -7,14 +7,14 @@ import 'package:flutter_quill/flutter_quill.dart'
import 'package:flutter_quill/translations.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../../models/config/image/editor/image_configurations.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../services/image_saver/s_image_saver.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../../utils/string.dart';
import '../../../utils/utils.dart';
import '../../widgets/image.dart' show ImageTapWrapper, getImageStyleString;
import '../../widgets/image_resizer.dart' show ImageResizer;
import '../../common/utils/element_utils/element_utils.dart';
import '../../common/utils/string.dart';
import '../../common/utils/utils.dart';
import '../../editor_toolbar_shared/image_saver/s_image_saver.dart';
import '../../editor_toolbar_shared/shared_configurations.dart';
import 'models/image_configurations.dart';
import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString;
import 'widgets/image_resizer.dart' show ImageResizer;
class ImageOptionsMenu extends StatelessWidget {
const ImageOptionsMenu({

@ -3,12 +3,12 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:universal_html/html.dart' as html;
import '../../../models/config/image/editor/image_web_configurations.dart';
import '../../../utils/dart_ui/dart_ui_fake.dart'
if (dart.library.js_interop) '../../../utils/dart_ui/dart_ui_real.dart'
import '../../common/utils/dart_ui/dart_ui_fake.dart'
if (dart.library.js_interop) '../../common/utils/dart_ui/dart_ui_real.dart'
as ui;
import '../../../utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart';
import '../../common/utils/element_utils/element_web_utils.dart';
import '../../common/utils/utils.dart';
import 'models/image_web_configurations.dart';
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
const QuillEditorWebImageEmbedBuilder({

@ -3,7 +3,7 @@ import 'dart:io' show File;
import 'package:flutter_quill/extensions.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/image/editor/image_embed_types.dart';
import '../image_embed_types.dart';
/// [QuillEditorImageEmbedConfigurations] for desktop, mobile and
/// other platforms

@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:photo_view/photo_view.dart';
import '../../models/config/image/editor/image_configurations.dart';
import '../../utils/utils.dart';
import '../image/editor/image_embed_types.dart';
import '../../../common/utils/utils.dart';
import '../image_embed_types.dart';
import '../models/image_configurations.dart';
const List<String> imageFileExtensions = [
'.jpeg',

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import '../../../utils/quill_table_utils.dart';
import '../../common/utils/quill_table_utils.dart';
import 'table_cell_embed.dart';
import 'table_models.dart';

@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import '../../../models/config/video/editor/video_configurations.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../../utils/utils.dart';
import '../../widgets/video_app.dart';
import '../../widgets/youtube_video_app.dart';
import '../../common/utils/element_utils/element_utils.dart';
import '../../common/utils/utils.dart';
import 'models/video_configurations.dart';
import 'widgets/video_app.dart';
import 'widgets/youtube_video_app.dart';
class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
const QuillEditorVideoEmbedBuilder({

@ -4,12 +4,12 @@ import 'package:universal_html/html.dart' as html;
import 'package:youtube_player_flutter/youtube_player_flutter.dart'
show YoutubePlayer;
import '../../../models/config/video/editor/video_web_configurations.dart';
import '../../../utils/dart_ui/dart_ui_fake.dart'
if (dart.library.js_interop) '../../../utils/dart_ui/dart_ui_real.dart'
import '../../common/utils/dart_ui/dart_ui_fake.dart'
if (dart.library.js_interop) '../../common/utils/dart_ui/dart_ui_real.dart'
as ui;
import '../../../utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart';
import '../../common/utils/element_utils/element_web_utils.dart';
import '../../common/utils/utils.dart';
import 'models/video_web_configurations.dart';
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
const QuillEditorWebVideoEmbedBuilder({

@ -6,7 +6,7 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart';
import '../../flutter_quill_extensions.dart';
import '../../../../flutter_quill_extensions.dart';
/// Widget for playing back video
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player

@ -5,7 +5,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../../models/config/video/editor/youtube_video_support_mode.dart';
import '../models/youtube_video_support_mode.dart';
import 'video_app.dart';
class YoutubeVideoApp extends StatefulWidget {

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../services/image_picker/s_image_picker.dart';
import '../../services/image_saver/s_image_saver.dart';
import 'image_picker/s_image_picker.dart';
import 'image_saver/s_image_saver.dart';
/// Configurations for Flutter Editor Extensions
/// shared between toolbar and editor

@ -2,23 +2,21 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_quill/flutter_quill.dart' as fq;
import 'package:meta/meta.dart' show immutable;
import 'embeds/image/editor/image_embed.dart';
import 'embeds/image/editor/image_web_embed.dart';
import 'embeds/image/toolbar/image_button.dart';
import 'embeds/others/camera_button/camera_button.dart';
import 'embeds/table/editor/table_embed.dart';
import 'embeds/table/toolbar/table_button.dart';
import 'embeds/video/editor/video_embed.dart';
import 'embeds/video/editor/video_web_embed.dart';
import 'embeds/video/toolbar/video_button.dart';
import 'models/config/camera/camera_configurations.dart';
import 'models/config/image/editor/image_configurations.dart';
import 'models/config/image/toolbar/image_configurations.dart';
import 'models/config/media/media_button_configurations.dart';
import 'models/config/table/table_configurations.dart';
import 'models/config/video/editor/video_configurations.dart';
import 'models/config/video/editor/video_web_configurations.dart';
import 'models/config/video/toolbar/video_configurations.dart';
import 'editor/image/image_embed.dart';
import 'editor/image/models/image_configurations.dart';
import 'editor/table/table_embed.dart';
import 'editor/video/models/video_configurations.dart';
import 'editor/video/models/video_web_configurations.dart';
import 'editor/video/video_embed.dart';
import 'editor/video/video_web_embed.dart';
import 'toolbar/camera/camera_button.dart';
import 'toolbar/camera/models/camera_configurations.dart';
import 'toolbar/image/image_button.dart';
import 'toolbar/image/models/image_configurations.dart';
import 'toolbar/table/models/table_configurations.dart';
import 'toolbar/table/table_button.dart';
import 'toolbar/video/models/video_configurations.dart';
import 'toolbar/video/video_button.dart';
@immutable
class FlutterQuillEmbeds {
@ -122,10 +120,6 @@ class FlutterQuillEmbeds {
const QuillToolbarVideoButtonOptions(),
QuillToolbarCameraButtonOptions? cameraButtonOptions,
QuillToolbarTableButtonOptions? tableButtonOptions,
@Deprecated(
'Media button has been removed, the value of this parameter will be ignored',
)
QuillToolbarMediaButtonOptions? mediaButtonOptions,
}) =>
[
if (imageButtonOptions != null)

@ -10,10 +10,10 @@ import 'package:flutter_quill/flutter_quill.dart'
kDefaultIconButtonFactor;
import 'package:flutter_quill/translations.dart';
import '../../../models/config/camera/camera_configurations.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../services/image_picker/image_options.dart';
import '../../editor_toolbar_shared/image_picker/image_options.dart';
import '../../editor_toolbar_shared/shared_configurations.dart';
import 'camera_types.dart';
import 'models/camera_configurations.dart';
import 'select_camera_action.dart';
class QuillToolbarCameraButton extends StatelessWidget {

@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart' show BuildContext;
import 'package:meta/meta.dart' show immutable;
import '../../image/editor/image_embed_types.dart';
import '../../video/video.dart';
import '../../editor/image/image_embed_types.dart';
import '../video/models/video.dart';
enum CameraAction {
video,

@ -1,6 +1,6 @@
import 'package:flutter_quill/flutter_quill.dart';
import '../../../embeds/others/camera_button/camera_types.dart';
import '../camera_types.dart';
class QuillToolbarCameraButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import '../../../models/config/formula/formula_configurations.dart';
import 'models/formula_configurations.dart';
class QuillToolbarFormulaButton extends StatelessWidget {
const QuillToolbarFormulaButton({

@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart';
import '../../../models/config/image/toolbar/image_configurations.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../services/image_picker/image_picker.dart';
import '../../others/image_video_utils.dart';
import '../editor/image_embed_types.dart';
import '../../common/image_video_utils.dart';
import '../../editor/image/image_embed_types.dart';
import '../../editor_toolbar_shared/image_picker/image_picker.dart';
import '../../editor_toolbar_shared/shared_configurations.dart';
import 'models/image_configurations.dart';
import 'select_image_source.dart';
class QuillToolbarImageButton extends StatelessWidget {

@ -1,7 +1,7 @@
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/image/editor/image_embed_types.dart';
import '../../../editor/image/image_embed_types.dart';
class QuillToolbarImageButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop;
import 'package:flutter_quill/translations.dart';
import '../editor/image_embed_types.dart';
import '../../editor/image/image_embed_types.dart';
class SelectImageSourceDialog extends StatelessWidget {
const SelectImageSourceDialog({super.key});

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart';
import '../../../models/config/table/table_configurations.dart';
import '../../../utils/quill_table_utils.dart';
import '../../common/utils/quill_table_utils.dart';
import 'models/table_configurations.dart';
class QuillToolbarTableButton extends StatelessWidget {
const QuillToolbarTableButton({

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../extensions/controller_ext.dart';
import '../../services/image_picker/s_image_picker.dart';
import '../../../common/extensions/controller_ext.dart';
import '../../../editor_toolbar_shared/image_picker/s_image_picker.dart';
/// When request picking an video, for example when the video button toolbar
/// clicked, it should be null in case the user didn't choose any video or

@ -1,6 +1,6 @@
import 'package:flutter_quill/flutter_quill.dart';
import '../../../../embeds/video/video.dart';
import 'video.dart';
class QuillToolbarVideoButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop;
import 'package:flutter_quill/translations.dart';
import '../video.dart';
import 'models/video.dart';
class SelectVideoSourceDialog extends StatelessWidget {
const SelectVideoSourceDialog({super.key});

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../models/config/video/toolbar/video_configurations.dart';
import '../../../services/image_picker/image_options.dart';
import '../../others/image_video_utils.dart';
import '../video.dart';
import '../../common/image_video_utils.dart';
import '../../editor_toolbar_shared/image_picker/image_options.dart';
import '../../editor_toolbar_shared/shared_configurations.dart';
import 'models/video.dart';
import 'models/video_configurations.dart';
import 'select_video_source.dart';
// TODO: Add custom callback to validate the video link input

@ -1,6 +1,6 @@
name: flutter_quill_extensions
description: Embed extensions for flutter_quill including image, video, formula and etc.
version: 10.1.10
version: 10.2.0
homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/
repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -4,6 +4,14 @@
All notable changes to this project will be documented in this file.
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,6 +1,6 @@
name: flutter_quill_test
description: Test utilities for flutter_quill which includes methods to simplify interacting with the editor in test cases.
version: 10.1.10
version: 10.2.0
homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/
repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -192,11 +192,12 @@ class Document {
while ((res.node as Line).length == 1 && index > 0) {
res = queryChild(--index);
}
// Get inline attributes from previous line
// Get inline attributes from previous line (link does not cross line breaks)
final prev = (res.node as Line).collectStyle(res.offset, 0);
final attributes = <String, Attribute>{};
for (final attr in prev.attributes.values) {
if (attr.scope == AttributeScope.inline) {
if (attr.scope == AttributeScope.inline &&
attr.key != Attribute.link.key) {
attributes[attr.key] = attr;
}
}
@ -211,13 +212,15 @@ class Document {
//
final style = (res.node as Line).collectStyle(res.offset - 1, 0);
final linkAttribute = style.attributes[Attribute.link.key];
if ((linkAttribute != null) &&
(linkAttribute.value !=
(res.node as Line)
.collectStyle(res.offset, len)
.attributes[Attribute.link.key]
?.value)) {
return style.removeAll({linkAttribute});
if (linkAttribute != null) {
if ((res.node!.length - 1 == res.offset) ||
(linkAttribute.value !=
(res.node as Line)
.collectStyle(res.offset, len)
.attributes[Attribute.link.key]
?.value)) {
return style.removeAll({linkAttribute});
}
}
return style;
}

@ -383,15 +383,34 @@ base class Line extends QuillContainer<Leaf?> {
pos += node.length;
}
}
result = result.mergeAll(style);
/// Blank lines do not have style and must get the active style from prior line
if (isEmpty) {
var prevLine = previous;
while (prevLine is Block && prevLine.isNotEmpty) {
prevLine = prevLine.children.last;
}
if (prevLine is Line) {
result = result.mergeAll(prevLine.collectStyle(prevLine.length - 1, 1));
}
} else {
result = result.mergeAll(style);
}
if (parent is Block) {
final block = parent as Block;
result = result.mergeAll(block.style);
}
final remaining = len - local;
if (remaining > 0 && nextLine != null) {
final rest = nextLine!.collectStyle(0, remaining);
var remaining = len - local;
var nxt = nextLine;
/// Skip over empty lines that have no attributes
while (remaining > 0 && nxt != null && nxt.isEmpty) {
remaining--;
nxt = nxt.nextLine;
}
if (remaining > 0 && nxt != null) {
final rest = nxt.collectStyle(0, remaining);
handle(rest);
}

@ -911,11 +911,22 @@ class RenderEditor extends RenderEditableContainerBox
final extentNode = _container.queryChild(textSelection.end, false).node;
RenderEditableBox? extentChild = baseChild;
while (extentChild != null) {
if (extentChild.container == extentNode) {
break;
/// Trap shortening the text of a link which can cause selection to extend off end of line
if (extentNode == null) {
while (true) {
final next = childAfter(extentChild);
if (next == null) {
break;
}
}
} else {
while (extentChild != null) {
if (extentChild.container == extentNode) {
break;
}
extentChild = childAfter(extentChild);
}
extentChild = childAfter(extentChild);
}
assert(extentChild != null);

@ -79,6 +79,19 @@ mixin RawEditorStateTextInputClientMixin on EditorState
_updateComposingRectIfNeeded();
//update IME position for Macos
_updateCaretRectIfNeeded();
/// Trap selection extends off end of document
if (_lastKnownRemoteTextEditingValue != null) {
if (_lastKnownRemoteTextEditingValue!.selection.end >
_lastKnownRemoteTextEditingValue!.text.length) {
_lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!
.copyWith(
selection: _lastKnownRemoteTextEditingValue!.selection
.copyWith(
extentOffset:
_lastKnownRemoteTextEditingValue!.text.length));
}
}
_textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!);
}
_textInputConnection!.show();

@ -560,16 +560,21 @@ class PreserveInlineStylesRule extends InsertRule {
final itr = DeltaIterator(documentDelta);
len ??= 0;
var prev = itr.skip(len == 0 ? index : index + 1);
var excludeLinkAtLineStart = false;
var excludeLink = false;
/// Process simple insertions at start of line
if (len == 0) {
final currLine = itr.next();
/// Trap for previous is not text with attributes
/// Prevent links extending beyond the link's text label.
excludeLink =
currLine.attributes?.containsKey(Attribute.link.key) != true &&
prev?.attributes?.containsKey(Attribute.link.key) == true;
/// Trap for previous is not text
if (prev?.data is! String) {
prev = currLine;
excludeLinkAtLineStart = true;
excludeLink = true;
} else {
final prevData = prev!.data as String;
if (prevData.endsWith('\n')) {
@ -580,13 +585,17 @@ class PreserveInlineStylesRule extends InsertRule {
if (prevData.trimRight().isEmpty) {
final back =
DeltaIterator(documentDelta).skip(index - prevData.length);
if (back != null && back.data is String) {
/// Prevent link attribute from propagating over line break
if (back != null &&
back.data is String &&
back.attributes?.containsKey(Attribute.link.key) != true) {
prev = back;
}
}
} else {
prev = currLine;
excludeLinkAtLineStart = true;
excludeLink = true;
}
}
}
@ -604,7 +613,7 @@ class PreserveInlineStylesRule extends InsertRule {
return null;
}
if (excludeLinkAtLineStart) {
if (excludeLink) {
attributes.remove(Attribute.link.key);
}
return Delta()

@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter.
version: 10.1.10
version: 10.2.0
homepage: https://1o24bbs.com/c/bulletjournal/108/
repository: https://github.com/singerdmx/flutter-quill/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -4,6 +4,32 @@ import 'package:test/test.dart';
void main() {
group('collectStyle', () {
test('No selection', () {
final delta = Delta()
..insert('plain\n')
..insert('bold\n', <String, dynamic>{'bold': true})
..insert('italic\n', <String, dynamic>{'italic': true});
final document = Document.fromDelta(delta);
//
expect(
document.getPlainText(0, document.length), 'plain\nbold\nitalic\n');
expect(document.length, 18);
//
for (var index = 0; index < 6; index++) {
expect(const Style(), document.collectStyle(index, 0));
}
//
for (var index = 6; index < 11; index++) {
expect(const Style.attr({'bold': Attribute.bold}),
document.collectStyle(index, 0));
}
//
for (var index = 11; index < document.length; index++) {
expect(const Style.attr({'italic': Attribute.italic}),
document.collectStyle(index, 0));
}
});
/// Lists and alignments have the same block attribute key but can have different values.
/// Changing the format value updates the document but must also update the toolbar button state
/// by ensuring the collectStyles method returns the attribute selected for the newly entered line.
@ -133,5 +159,27 @@ void main() {
//
expect(const Style(), document.collectStyle(3, 3));
});
/// Links do not cross a line boundary
/// Enter key inserts newline as plain text without inline styles.
/// collectStyle needs to retrieve style of preceding line
test('Links and line boundaries', () {
final delta = Delta()
..insert('A link ')
..insert('home page', <String, dynamic>{'link': 'https://unknown.com'})
..insert('\n\nplain\n');
final document = Document.fromDelta(delta);
//
const linkStyle =
Style.attr({'link': LinkAttribute('https://unknown.com')});
//
expect(document.collectStyle(15, 0), linkStyle, reason: 'Within Link');
expect(document.collectStyle(16, 0), const Style(),
reason: 'At end of link');
expect(document.collectStyle(17, 0), const Style(),
reason: 'start of blank line');
expect(document.collectStyle(18, 0), const Style(),
reason: 'start of blank line');
});
});
}

@ -0,0 +1,83 @@
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:test/test.dart';
void main() {
group('collectStyle', () {
test('Simple', () {
final delta = Delta()
..insert('First\nSecond ')
..insert('Bold', <String, dynamic>{'bold': true})
..insert('\n\nplain\n');
final document = Document.fromDelta(delta);
//
final line = document.queryChild(6).node as Line;
expect(line.getPlainText(0, line.length), 'Second Bold\n');
expect(line.length, 12);
//
expect(line.collectStyle(0, line.length), const Style());
expect(
line.collectStyle(7, 4), const Style.attr({'bold': Attribute.bold}));
expect(
line.collectStyle(7, 5), const Style.attr({'bold': Attribute.bold}),
reason: 'Include trailing NL');
expect(
line.collectStyle(7, 6), const Style.attr({'bold': Attribute.bold}),
reason: 'Spans next NL');
expect(line.collectStyle(7, 7), const Style(),
reason: 'Spans into plain text');
//
final line2 = document.queryChild(18).node as Line;
expect(line2.length, 1);
expect(
line2.collectStyle(0, 1), const Style.attr({'bold': Attribute.bold}),
reason: 'Empty line gets style from previous line');
});
test('Block', () {
final delta = Delta()
..insert('first', {'bold': true})
..insert('\n', {'list': Attribute.ol})
..insert('second', {'bold': true})
..insert('\n', {'list': Attribute.ol})
..insert('third', {'italic': true})
..insert('\n', {'list': Attribute.ol})
..insert('\nplain\n');
final document = Document.fromDelta(delta);
//
const orderedList = Attribute('list', AttributeScope.block, Attribute.ol);
expect(document.collectStyle(0, 4),
const Style.attr({'bold': Attribute.bold, 'list': orderedList}));
//
final first = document.queryChild(1).node as Line;
expect(first.getPlainText(0, first.length), 'first\n');
expect(first.length, 6);
expect(first.collectStyle(0, 2),
const Style.attr({'bold': Attribute.bold, 'list': orderedList}));
//
final second = document.queryChild(6).node as Line;
expect(second.getPlainText(0, second.length), 'second\n');
expect(second.length, 7);
expect(second.collectStyle(2, 4),
const Style.attr({'bold': Attribute.bold, 'list': orderedList}));
//
expect(first.collectStyle(3, 5),
const Style.attr({'bold': Attribute.bold, 'list': orderedList}),
reason: 'spans first and second list entry');
expect(second.collectStyle(3, 6), const Style.attr({'list': orderedList}),
reason: 'spans second and third list entry');
//
final plain = document.queryChild(20).node as Line;
expect(plain.getPlainText(0, plain.length), 'plain\n');
expect(plain.length, 6);
expect(plain.collectStyle(2, 4), const Style());
//
final blank = document.queryChild(19).node as Line;
expect(blank.getPlainText(0, blank.length), '\n');
expect(blank.length, 1);
expect(blank.getPlainText(0, 1), '\n');
expect(blank.collectStyle(0, 1),
const Style.attr({'italic': Attribute.italic, 'list': orderedList}));
});
});
}
Loading…
Cancel
Save