diff --git a/CHANGELOG.md b/CHANGELOG.md index 7184615e..4172b66f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [7.10.0] +- **Breaking change**: `QuillToolbar.basic()` can be accessed from `QuillToolbar()` directly and the old `QuillToolbar` can be accessed from `QuillBaseToolbar` +- The Quill editor and toolbar configurations are now refactored in one single class for each one +- After changing one of the checkbox list values the controller will not request the keyboard focus by default +- We have moved the configurations of the toolbar and the editor directly into the widget but we are still using inherited widgets internally +- Fixes to some of the code after the refactoring + ## [7.9.0] - Buttons Improvemenets - Refactor all the button configurations that used in `QuillToolbar.basic()` but there are still few lefts diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 5321e453..fffc4abb 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'dart:async'; import 'dart:convert'; import 'dart:io' show File, Platform; @@ -186,12 +188,47 @@ class _HomePageState extends State { QuillEditor get quillEditor { if (kIsWeb) { return QuillEditor( - scrollController: ScrollController(), - scrollable: true, focusNode: _focusNode, + scrollController: ScrollController(), + configurations: QuillEditorConfigurations( + placeholder: 'Add content', + readOnly: false, + scrollable: true, + autoFocus: false, + expands: false, + padding: EdgeInsets.zero, + onTapUp: (details, p1) { + return _onTripleClickSelection(); + }, + customStyles: const DefaultStyles( + h1: DefaultTextBlockStyle( + TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + VerticalSpacing(16, 0), + VerticalSpacing(0, 0), + null), + sizeSmall: TextStyle(fontSize: 9), + ), + embedBuilders: [ + ...defaultEmbedBuildersWeb, + TimeStampEmbedBuilderWidget() + ], + ), + ); + } + return QuillEditor( + configurations: QuillEditorConfigurations( + placeholder: 'Add content', + readOnly: false, autoFocus: false, + enableSelectionToolbar: isMobile(), expands: false, padding: EdgeInsets.zero, + onImagePaste: _onImagePaste, onTapUp: (details, p1) { return _onTripleClickSelection(); }, @@ -207,87 +244,79 @@ class _HomePageState extends State { VerticalSpacing(0, 0), null), sizeSmall: TextStyle(fontSize: 9), + subscript: TextStyle( + fontFamily: 'SF-UI-Display', + fontFeatures: [FontFeature.subscripts()], + ), + superscript: TextStyle( + fontFamily: 'SF-UI-Display', + fontFeatures: [FontFeature.superscripts()], + ), ), embedBuilders: [ - ...defaultEmbedBuildersWeb, + ...FlutterQuillEmbeds.builders(), TimeStampEmbedBuilderWidget() ], - ); - } - return QuillEditor( + ), scrollController: ScrollController(), - scrollable: true, focusNode: _focusNode, - autoFocus: false, - enableSelectionToolbar: isMobile(), - expands: false, - padding: EdgeInsets.zero, - onImagePaste: _onImagePaste, - onTapUp: (details, p1) { - return _onTripleClickSelection(); - }, - customStyles: const DefaultStyles( - h1: DefaultTextBlockStyle( - TextStyle( - fontSize: 32, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - VerticalSpacing(16, 0), - VerticalSpacing(0, 0), - null), - sizeSmall: TextStyle(fontSize: 9), - subscript: TextStyle( - fontFamily: 'SF-UI-Display', - fontFeatures: [FontFeature.subscripts()], - ), - superscript: TextStyle( - fontFamily: 'SF-UI-Display', - fontFeatures: [FontFeature.superscripts()], - ), - ), - embedBuilders: [ - ...FlutterQuillEmbeds.builders(), - TimeStampEmbedBuilderWidget() - ], ); } QuillToolbar get quillToolbar { if (kIsWeb) { - return QuillToolbar.basic( - embedButtons: FlutterQuillEmbeds.buttons( - onImagePickCallback: _onImagePickCallback, - webImagePickImpl: _webImagePickImpl, + return QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.buttons( + onImagePickCallback: _onImagePickCallback, + webImagePickImpl: _webImagePickImpl, + ), + buttonOptions: QuillToolbarButtonOptions( + base: QuillToolbarBaseButtonOptions( + afterButtonPressed: _focusNode.requestFocus, + ), + ), ), - showAlignmentButtons: true, // afterButtonPressed: _focusNode.requestFocus, ); } if (_isDesktop()) { - return QuillToolbar.basic( - embedButtons: FlutterQuillEmbeds.buttons( - onImagePickCallback: _onImagePickCallback, - filePickImpl: openFileSystemPickerForDesktop, + return QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.buttons( + onImagePickCallback: _onImagePickCallback, + filePickImpl: openFileSystemPickerForDesktop, + ), + showAlignmentButtons: true, + buttonOptions: QuillToolbarButtonOptions( + base: QuillToolbarBaseButtonOptions( + afterButtonPressed: _focusNode.requestFocus, + ), + ), ), - showAlignmentButtons: true, // afterButtonPressed: _focusNode.requestFocus, ); } - return QuillToolbar.basic( - embedButtons: FlutterQuillEmbeds.buttons( - // provide a callback to enable picking images from device. - // if omit, "image" button only allows adding images from url. - // same goes for videos. - onImagePickCallback: _onImagePickCallback, - onVideoPickCallback: _onVideoPickCallback, - // uncomment to provide a custom "pick from" dialog. - // mediaPickSettingSelector: _selectMediaPickSetting, - // uncomment to provide a custom "pick from" dialog. - // cameraPickSettingSelector: _selectCameraPickSetting, + return QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.buttons( + // provide a callback to enable picking images from device. + // if omit, "image" button only allows adding images from url. + // same goes for videos. + onImagePickCallback: _onImagePickCallback, + onVideoPickCallback: _onVideoPickCallback, + // uncomment to provide a custom "pick from" dialog. + // mediaPickSettingSelector: _selectMediaPickSetting, + // uncomment to provide a custom "pick from" dialog. + // cameraPickSettingSelector: _selectCameraPickSetting, + ), + showAlignmentButtons: true, + buttonOptions: QuillToolbarButtonOptions( + base: QuillToolbarBaseButtonOptions( + afterButtonPressed: _focusNode.requestFocus, + ), + ), ), - showAlignmentButtons: true, // afterButtonPressed: _focusNode.requestFocus, ); } @@ -320,17 +349,6 @@ class _HomePageState extends State { return SafeArea( child: QuillProvider( configurations: QuillConfigurations( - toolbarConfigurations: QuillToolbarConfigurations( - buttonOptions: QuillToolbarButtonOptions( - base: QuillToolbarBaseButtonOptions( - afterButtonPressed: _focusNode.requestFocus, - ), - )), - editorConfigurations: const QuillEditorConfigurations( - placeholder: 'Add content', - // ignore: avoid_redundant_argument_values - readOnly: false, - ), controller: _controller, ), child: Column( diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 3a700d86..5693f6b3 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/extensions.dart'; @@ -35,24 +37,28 @@ class _ReadOnlyPageState extends State { Widget _buildContent(BuildContext context, QuillController? controller) { var quillEditor = QuillEditor( + configurations: QuillEditorConfigurations( + expands: false, + padding: EdgeInsets.zero, + embedBuilders: FlutterQuillEmbeds.builders(), + scrollable: true, + autoFocus: true, + ), scrollController: ScrollController(), - scrollable: true, focusNode: _focusNode, - autoFocus: true, // readOnly: !_edit, - expands: false, - padding: EdgeInsets.zero, - embedBuilders: FlutterQuillEmbeds.builders(), ); if (kIsWeb) { quillEditor = QuillEditor( + configurations: QuillEditorConfigurations( + autoFocus: true, + expands: false, + padding: EdgeInsets.zero, + embedBuilders: defaultEmbedBuildersWeb, + scrollable: true, + ), scrollController: ScrollController(), - scrollable: true, focusNode: _focusNode, - autoFocus: true, - expands: false, - padding: EdgeInsets.zero, - embedBuilders: defaultEmbedBuildersWeb, ); } return Padding( diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 83a1845d..fb96b01c 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -86,14 +86,18 @@ class _DemoScaffoldState extends State { QuillToolbar get quillToolbar { if (_isDesktop()) { - return QuillToolbar.basic( - embedButtons: FlutterQuillEmbeds.buttons( - filePickImpl: openFileSystemPickerForDesktop, + return QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.buttons( + filePickImpl: openFileSystemPickerForDesktop, + ), ), ); } - return QuillToolbar.basic( - embedButtons: FlutterQuillEmbeds.buttons(), + return QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.buttons(), + ), ); } diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 9cef05c3..e8234cb2 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -26,6 +26,7 @@ export 'src/widgets/editor/editor.dart'; export 'src/widgets/embeds.dart'; export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction; export 'src/widgets/style_widgets/style_widgets.dart'; +export 'src/widgets/toolbar/base_toolbar.dart'; export 'src/widgets/toolbar/enum.dart'; export 'src/widgets/toolbar/toolbar.dart'; export 'src/widgets/utils/provider.dart'; diff --git a/lib/src/models/config/editor/configurations.dart b/lib/src/models/config/editor/configurations.dart index 98abf0ad..1ccbb758 100644 --- a/lib/src/models/config/editor/configurations.dart +++ b/lib/src/models/config/editor/configurations.dart @@ -1,12 +1,67 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart' show immutable; +import 'package:flutter/foundation.dart' show Brightness, Uint8List, immutable; +import 'package:flutter/material.dart' + show TextCapitalization, TextSelectionThemeData; +import 'package:flutter/widgets.dart'; + +import '../../../widgets/default_styles.dart'; +import '../../../widgets/delegate.dart'; +import '../../../widgets/editor/editor.dart'; +import '../../../widgets/embeds.dart'; +import '../../../widgets/link.dart'; +import '../../../widgets/raw_editor/raw_editor.dart'; +import '../../themes/quill_dialog_theme.dart'; /// The configurations for the quill editor widget of flutter quill @immutable class QuillEditorConfigurations extends Equatable { + /// Important note for the maintainers + /// When editing this class please update the [copyWith] function too. const QuillEditorConfigurations({ + this.scrollable = true, + this.padding = EdgeInsets.zero, + this.autoFocus = false, + this.expands = false, this.placeholder, this.readOnly = false, + this.textSelectionThemeData, + this.showCursor, + this.paintCursorAboveText, + this.enableInteractiveSelection = true, + this.enableSelectionToolbar = true, + this.scrollBottomInset = 0, + this.minHeight, + this.maxHeight, + this.maxContentWidth, + this.customStyles, + this.textCapitalization = TextCapitalization.sentences, + this.keyboardAppearance = Brightness.light, + this.scrollPhysics, + this.onLaunchUrl, + this.onTapDown, + this.onTapUp, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.embedBuilders, + this.unknownEmbedBuilder, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, + this.customStyleBuilder, + this.customRecognizerBuilder, + this.floatingCursorDisabled = false, + this.textSelectionControls, + this.onImagePaste, + this.customShortcuts, + this.customActions, + this.detectWordBoundary = true, + this.enableUnfocusOnTapOutside = true, + this.customLinkPrefixes = const [], + this.dialogTheme, + this.contentInsertionConfiguration, + this.contextMenuBuilder, + this.editorKey, + this.requestKeyboardFocusOnCheckListChanged = false, }); /// The text placeholder in the quill editor @@ -20,9 +75,315 @@ class QuillEditorConfigurations extends Equatable { /// Defaults to `false`. Must not be `null`. final bool readOnly; + /// Whether this editor should create a scrollable container for its content. + /// + /// When set to `true` the editor's height can be controlled by [minHeight], + /// [maxHeight] and [expands] properties. + /// + /// When set to `false` the editor always expands to fit the entire content + /// of the document and should normally be placed as a child of another + /// scrollable widget, otherwise the content may be clipped. + /// by default it will by true + final bool scrollable; + final double scrollBottomInset; + + /// Additional space around the content of this editor. + /// by default will be [EdgeInsets.zero] + final EdgeInsetsGeometry padding; + + /// Whether this editor should focus itself if nothing else is already + /// focused. + /// + /// If true, the keyboard will open as soon as this editor obtains focus. + /// Otherwise, the keyboard is only shown after the user taps the editor. + /// + /// Defaults to `false`. Cannot be `null`. + final bool autoFocus; + + /// Whether focus should be revoked on tap outside the editor. + final bool enableUnfocusOnTapOutside; + + /// Whether to show cursor. + /// + /// The cursor refers to the blinking caret when the editor is focused. + final bool? showCursor; + final bool? paintCursorAboveText; + + /// Whether to enable user interface affordances for changing the + /// text selection. + /// + /// For example, setting this to true will enable features such as + /// long-pressing the editor to select text and show the + /// cut/copy/paste menu, and tapping to move the text cursor. + /// + /// When this is false, the text selection cannot be adjusted by + /// the user, text cannot be copied, and the user cannot paste into + /// the text field from the clipboard. + /// + /// To disable just the selection toolbar, set enableSelectionToolbar + /// to false. + final bool enableInteractiveSelection; + + /// Whether to show the cut/copy/paste menu when selecting text. + final bool enableSelectionToolbar; + + /// The minimum height to be occupied by this editor. + /// + /// This only has effect if [scrollable] is set to `true` and [expands] is + /// set to `false`. + final double? minHeight; + + /// The maximum height to be occupied by this editor. + /// + /// This only has effect if [scrollable] is set to `true` and [expands] is + /// set to `false`. + final double? maxHeight; + + /// The maximum width to be occupied by the content of this editor. + /// + /// If this is not null and and this editor's width is larger than this value + /// then the contents will be constrained to the provided maximum width and + /// horizontally centered. This is mostly useful on devices with wide screens. + final double? maxContentWidth; + + /// Allows to override [DefaultStyles]. + final DefaultStyles? customStyles; + + /// Whether this editor's height will be sized to fill its parent. + /// + /// This only has effect if [scrollable] is set to `true`. + /// + /// If expands is set to true and wrapped in a parent widget like [Expanded] + /// or [SizedBox], the editor will expand to fill the parent. + /// + /// [maxHeight] and [minHeight] must both be `null` when this is set to + /// `true`. + /// + /// Defaults to `false`. + final bool expands; + + /// Configures how the platform keyboard will select an uppercase or + /// lowercase keyboard. + /// + /// Only supports text keyboards, other keyboard types will ignore this + /// configuration. Capitalization is locale-aware. + /// + /// Defaults to [TextCapitalization.sentences]. Must not be `null`. + final TextCapitalization textCapitalization; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// Defaults to [Brightness.light]. + final Brightness keyboardAppearance; + + /// The [ScrollPhysics] to use when vertically scrolling the input. + /// + /// This only has effect if [scrollable] is set to `true`. + /// + /// If not specified, it will behave according to the current platform. + /// + /// See [Scrollable.physics]. + final ScrollPhysics? scrollPhysics; + + /// Callback to invoke when user wants to launch a URL. + final ValueChanged? onLaunchUrl; + + // Returns whether gesture is handled + final bool Function( + TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + + // Returns whether gesture is handled + final bool Function( + TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + + // Returns whether gesture is handled + final bool Function( + LongPressStartDetails details, TextPosition Function(Offset offset))? + onSingleLongTapStart; + + // Returns whether gesture is handled + final bool Function(LongPressMoveUpdateDetails details, + TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; + + // Returns whether gesture is handled + final bool Function( + LongPressEndDetails details, TextPosition Function(Offset offset))? + onSingleLongTapEnd; + + final Iterable? embedBuilders; + final EmbedBuilder? unknownEmbedBuilder; + final CustomStyleBuilder? customStyleBuilder; + final CustomRecognizerBuilder? customRecognizerBuilder; + + /// Delegate function responsible for showing menu with link actions on + /// mobile platforms (iOS, Android). + /// + /// The menu is triggered in editing mode ([readOnly] is set to `false`) + /// when the user long-presses a link-styled text segment. + /// + /// FlutterQuill provides default implementation which can be overridden by + /// this field to customize the user experience. + /// + /// By default on iOS the menu is displayed with [showCupertinoModalPopup] + /// which constructs an instance of [CupertinoActionSheet]. For Android, + /// the menu is displayed with [showModalBottomSheet] and a list of + /// Material [ListTile]s. + final LinkActionPickerDelegate linkActionPickerDelegate; + + final bool floatingCursorDisabled; + + /// allows to create a custom textSelectionControls, + /// if this is null a default textSelectionControls based on the app's theme + /// will be used + final TextSelectionControls? textSelectionControls; + + /// Callback when the user pastes the given image. + /// + /// Returns the url of the image if the image should be inserted. + final Future Function(Uint8List imageBytes)? onImagePaste; + + /// Contains user-defined shortcuts map. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] + final Map? customShortcuts; + + /// Contains user-defined actions. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] + final Map>? customActions; + + final bool detectWordBoundary; + + /// Additional list if links prefixes, which must not be prepended + /// with "https://" when [LinkMenuAction.launch] happened + /// + /// Useful for deeplinks + final List customLinkPrefixes; + + /// Configures the dialog theme. + final QuillDialogTheme? dialogTheme; + + // Allows for creating a custom context menu + final QuillEditorContextMenuBuilder? contextMenuBuilder; + + /// Configuration of handler for media content inserted via the system input + /// method. + /// + /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] + final ContentInsertionConfiguration? contentInsertionConfiguration; + + /// Using the editorKey for get getLocalRectForCaret + /// editorKey.currentState?.renderEditor.getLocalRectForCaret + final GlobalKey? editorKey; + + /// By default we will use + /// ``` + /// TextSelectionTheme.of(context) + /// ``` + /// to change it please pass a different value + final TextSelectionThemeData? textSelectionThemeData; + + /// When there is a change the check list values + /// should we request keyboard focus?? + final bool requestKeyboardFocusOnCheckListChanged; + @override List get props => [ placeholder, readOnly, ]; + + // We might use code generator like freezed but sometimes it can be limitied + // instead whatever there is a change to the parameters in this class please + // regenerate this function using extension in vs code or plugin in intellij + QuillEditorConfigurations copyWith({ + String? placeholder, + bool? readOnly, + bool? scrollable, + double? scrollBottomInset, + EdgeInsetsGeometry? padding, + bool? autoFocus, + bool? enableUnfocusOnTapOutside, + bool? showCursor, + bool? paintCursorAboveText, + bool? enableInteractiveSelection, + bool? enableSelectionToolbar, + double? minHeight, + double? maxHeight, + double? maxContentWidth, + DefaultStyles? customStyles, + bool? expands, + TextCapitalization? textCapitalization, + Brightness? keyboardAppearance, + ScrollPhysics? scrollPhysics, + ValueChanged? onLaunchUrl, + Iterable? embedBuilders, + EmbedBuilder? unknownEmbedBuilder, + CustomStyleBuilder? customStyleBuilder, + CustomRecognizerBuilder? customRecognizerBuilder, + LinkActionPickerDelegate? linkActionPickerDelegate, + bool? floatingCursorDisabled, + TextSelectionControls? textSelectionControls, + Future Function(Uint8List imageBytes)? onImagePaste, + Map? customShortcuts, + Map>? customActions, + bool? detectWordBoundary, + List? customLinkPrefixes, + QuillDialogTheme? dialogTheme, + QuillEditorContextMenuBuilder? contextMenuBuilder, + ContentInsertionConfiguration? contentInsertionConfiguration, + GlobalKey? editorKey, + TextSelectionThemeData? textSelectionThemeData, + }) { + return QuillEditorConfigurations( + placeholder: placeholder ?? this.placeholder, + readOnly: readOnly ?? this.readOnly, + scrollable: scrollable ?? this.scrollable, + scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset, + padding: padding ?? this.padding, + autoFocus: autoFocus ?? this.autoFocus, + enableUnfocusOnTapOutside: + enableUnfocusOnTapOutside ?? this.enableUnfocusOnTapOutside, + showCursor: showCursor ?? this.showCursor, + paintCursorAboveText: paintCursorAboveText ?? this.paintCursorAboveText, + enableInteractiveSelection: + enableInteractiveSelection ?? this.enableInteractiveSelection, + enableSelectionToolbar: + enableSelectionToolbar ?? this.enableSelectionToolbar, + minHeight: minHeight ?? this.minHeight, + maxHeight: maxHeight ?? this.maxHeight, + maxContentWidth: maxContentWidth ?? this.maxContentWidth, + customStyles: customStyles ?? this.customStyles, + expands: expands ?? this.expands, + textCapitalization: textCapitalization ?? this.textCapitalization, + keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance, + scrollPhysics: scrollPhysics ?? this.scrollPhysics, + onLaunchUrl: onLaunchUrl ?? this.onLaunchUrl, + embedBuilders: embedBuilders ?? this.embedBuilders, + unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder, + customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder, + customRecognizerBuilder: + customRecognizerBuilder ?? this.customRecognizerBuilder, + linkActionPickerDelegate: + linkActionPickerDelegate ?? this.linkActionPickerDelegate, + floatingCursorDisabled: + floatingCursorDisabled ?? this.floatingCursorDisabled, + textSelectionControls: + textSelectionControls ?? this.textSelectionControls, + onImagePaste: onImagePaste ?? this.onImagePaste, + customShortcuts: customShortcuts ?? this.customShortcuts, + customActions: customActions ?? this.customActions, + detectWordBoundary: detectWordBoundary ?? this.detectWordBoundary, + customLinkPrefixes: customLinkPrefixes ?? this.customLinkPrefixes, + dialogTheme: dialogTheme ?? this.dialogTheme, + contextMenuBuilder: contextMenuBuilder ?? this.contextMenuBuilder, + contentInsertionConfiguration: + contentInsertionConfiguration ?? this.contentInsertionConfiguration, + editorKey: editorKey ?? this.editorKey, + textSelectionThemeData: + textSelectionThemeData ?? this.textSelectionThemeData, + ); + } } diff --git a/lib/src/models/config/quill_configurations.dart b/lib/src/models/config/quill_configurations.dart index e37a9fad..e3f9a233 100644 --- a/lib/src/models/config/quill_configurations.dart +++ b/lib/src/models/config/quill_configurations.dart @@ -11,8 +11,6 @@ export './toolbar/configurations.dart'; class QuillConfigurations extends Equatable { const QuillConfigurations({ required this.controller, - this.editorConfigurations = const QuillEditorConfigurations(), - this.toolbarConfigurations = const QuillToolbarConfigurations(), this.sharedConfigurations = const QuillSharedConfigurations(), }); @@ -24,20 +22,12 @@ class QuillConfigurations extends Equatable { /// here, it should not be null final QuillController controller; - /// The configurations for the quill editor widget of flutter quill - final QuillEditorConfigurations editorConfigurations; - - /// The configurations for the toolbar widget of flutter quill - final QuillToolbarConfigurations toolbarConfigurations; - /// The shared configurations between [QuillEditorConfigurations] and /// [QuillToolbarConfigurations] so we don't duplicate things final QuillSharedConfigurations sharedConfigurations; @override List get props => [ - editorConfigurations, - toolbarConfigurations, sharedConfigurations, ]; } diff --git a/lib/src/models/config/shared_configurations.dart b/lib/src/models/config/shared_configurations.dart index ef6eb3a2..78dd6b06 100644 --- a/lib/src/models/config/shared_configurations.dart +++ b/lib/src/models/config/shared_configurations.dart @@ -39,6 +39,7 @@ class QuillSharedConfigurations extends Equatable { @override List get props => [ dialogBarrierColor, + dialogTheme, locale, animationConfigurations, ]; diff --git a/lib/src/models/config/toolbar/base_configurations.dart b/lib/src/models/config/toolbar/base_configurations.dart new file mode 100644 index 00000000..8b66db62 --- /dev/null +++ b/lib/src/models/config/toolbar/base_configurations.dart @@ -0,0 +1,63 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/widgets.dart' + show Axis, Color, Decoration, WrapAlignment, WrapCrossAlignment, immutable; + +import '../../../widgets/toolbar/base_toolbar.dart'; +import '../../structs/link_dialog_action.dart'; +import '../../themes/quill_custom_button.dart'; + +@immutable +class QuillBaseToolbarConfigurations extends Equatable { + const QuillBaseToolbarConfigurations({ + required this.childrenBuilder, + this.axis = Axis.horizontal, + this.toolbarSize = kDefaultIconSize * 2, + this.toolbarSectionSpacing = kToolbarSectionSpacing, + this.toolbarIconAlignment = WrapAlignment.center, + this.toolbarIconCrossAlignment = WrapCrossAlignment.center, + this.color, + this.customButtons = const [], + this.sectionDividerColor, + this.sectionDividerSpace, + this.linkDialogAction, + this.multiRowsDisplay = true, + this.decoration, + }); + + final QuillBaseToolbarChildrenBuilder childrenBuilder; + final Axis axis; + final double toolbarSectionSpacing; + final WrapAlignment toolbarIconAlignment; + final WrapCrossAlignment toolbarIconCrossAlignment; + final double toolbarSize; + + // Overrides the action in the _LinkDialog widget + final LinkDialogAction? linkDialogAction; + + /// The color of the toolbar. + /// + /// Defaults to [ThemeData.canvasColor] of the current [Theme] if no color + /// is given. + final Color? color; + + /// List of custom buttons + final List customButtons; + + /// The color to use when painting the toolbar section divider. + /// + /// If this is null, then the [DividerThemeData.color] is used. If that is + /// also null, then [ThemeData.dividerColor] is used. + final Color? sectionDividerColor; + + /// The space occupied by toolbar section divider. + final double? sectionDividerSpace; + + /// If you want the toolbar to not be a multiple rows pass false + final bool multiRowsDisplay; + + /// The decoration to use for the toolbar. + final Decoration? decoration; + + @override + List get props => throw UnimplementedError(); +} diff --git a/lib/src/models/config/toolbar/buttons/link_style.dart b/lib/src/models/config/toolbar/buttons/link_style.dart index d7860b0d..7e2b6717 100644 --- a/lib/src/models/config/toolbar/buttons/link_style.dart +++ b/lib/src/models/config/toolbar/buttons/link_style.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart' show Color; -import '../../../../widgets/toolbar/toolbar.dart'; +import '../../../../widgets/toolbar/base_toolbar.dart'; import '../../../structs/link_dialog_action.dart'; import '../../../themes/quill_dialog_theme.dart'; diff --git a/lib/src/models/config/toolbar/buttons/select_header_style.dart b/lib/src/models/config/toolbar/buttons/select_header_style.dart index 4a742582..1145afaf 100644 --- a/lib/src/models/config/toolbar/buttons/select_header_style.dart +++ b/lib/src/models/config/toolbar/buttons/select_header_style.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart' show Axis; -import '../../../../widgets/toolbar/toolbar.dart'; +import '../../../../widgets/toolbar/base_toolbar.dart'; import '../../../documents/attribute.dart'; class QuillToolbarSelectHeaderStyleButtonExtraOptions diff --git a/lib/src/models/config/toolbar/configurations.dart b/lib/src/models/config/toolbar/configurations.dart index a0f94c6d..819b7bed 100644 --- a/lib/src/models/config/toolbar/configurations.dart +++ b/lib/src/models/config/toolbar/configurations.dart @@ -1,7 +1,13 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart' show immutable; -import 'package:flutter/widgets.dart' show Axis; +import 'package:flutter/widgets.dart' + show Axis, Color, Decoration, WrapAlignment, WrapCrossAlignment; +import '../../../widgets/embeds.dart'; +import '../../structs/link_dialog_action.dart'; +import '../../themes/quill_custom_button.dart'; +import '../../themes/quill_dialog_theme.dart'; +import '../../themes/quill_icon_theme.dart'; import 'buttons/base.dart'; import 'buttons/clear_format.dart'; import 'buttons/color.dart'; @@ -47,11 +53,60 @@ const double kToolbarSectionSpacing = 4; @immutable class QuillToolbarConfigurations extends Equatable { const QuillToolbarConfigurations({ + this.toolbarSectionSpacing = kToolbarSectionSpacing, + this.toolbarIconAlignment = WrapAlignment.center, + this.toolbarIconCrossAlignment = WrapCrossAlignment.center, this.buttonOptions = const QuillToolbarButtonOptions(), this.multiRowsDisplay = true, this.fontFamilyValues, this.fontSizesValues, + this.showDividers = true, + this.showFontFamily = true, + this.showFontSize = true, + this.showBoldButton = true, + this.showItalicButton = true, + this.showSmallButton = false, + this.showUnderLineButton = true, + this.showStrikeThrough = true, + this.showInlineCode = true, + this.showColorButton = true, + this.showBackgroundColorButton = true, + this.showClearFormat = true, + this.showAlignmentButtons = false, + this.showLeftAlignment = true, + this.showCenterAlignment = true, + this.showRightAlignment = true, + this.showJustifyAlignment = true, + this.showHeaderStyle = true, + this.showListNumbers = true, + this.showListBullets = true, + this.showListCheck = true, + this.showCodeBlock = true, + this.showQuote = true, + this.showIndent = true, + this.showLink = true, + this.showUndo = true, + this.showRedo = true, + this.showDirection = false, + this.showSearchButton = true, + this.showSubscript = true, + this.showSuperscript = true, + this.customButtons = const [], + + /// The decoration to use for the toolbar. + this.decoration, + + /// Toolbar items to display for controls of embed blocks + this.embedButtons, + this.linkDialogAction, + + ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme] + this.iconTheme, + this.dialogTheme, this.axis = Axis.horizontal, + this.color, + this.sectionDividerColor, + this.sectionDividerSpace, /// By default it will calculated based on the [globalIconSize] from /// [base] in [QuillToolbarButtonOptions] @@ -109,6 +164,67 @@ class QuillToolbarConfigurations extends Equatable { /// we will update that logic soon final Axis axis; + // Overrides the action in the _LinkDialog widget + final LinkDialogAction? linkDialogAction; + + final double toolbarSectionSpacing; + final WrapAlignment toolbarIconAlignment; + final WrapCrossAlignment toolbarIconCrossAlignment; + final bool showDividers; + final bool showFontFamily; + final bool showFontSize; + final bool showBoldButton; + final bool showItalicButton; + final bool showSmallButton; + final bool showUnderLineButton; + final bool showStrikeThrough; + final bool showInlineCode; + final bool showColorButton; + final bool showBackgroundColorButton; + final bool showClearFormat; + final bool showAlignmentButtons; + final bool showLeftAlignment; + final bool showCenterAlignment; + final bool showRightAlignment; + final bool showJustifyAlignment; + final bool showHeaderStyle; + final bool showListNumbers; + final bool showListBullets; + final bool showListCheck; + final bool showCodeBlock; + final bool showQuote; + final bool showIndent; + final bool showLink; + final bool showUndo; + final bool showRedo; + final bool showDirection; + final bool showSearchButton; + final bool showSubscript; + final bool showSuperscript; + final List customButtons; + + /// The decoration to use for the toolbar. + final Decoration? decoration; + + /// Toolbar items to display for controls of embed blocks + final List? embedButtons; + + ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme] + final QuillIconTheme? iconTheme; + + ///The theme to use for the theming of the [LinkDialog()], + ///shown when embedding an image, for example + final QuillDialogTheme? dialogTheme; + + /// The color of the toolbar + final Color? color; + + /// The color of the toolbar section divider + final Color? sectionDividerColor; + + /// The space occupied by toolbar divider + final double? sectionDividerSpace; + @override List get props => [ buttonOptions, diff --git a/lib/src/models/themes/quill_custom_button.dart b/lib/src/models/themes/quill_custom_button.dart index 7caee256..da1eb06d 100644 --- a/lib/src/models/themes/quill_custom_button.dart +++ b/lib/src/models/themes/quill_custom_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../widgets/toolbar/toolbar.dart'; +import '../../widgets/toolbar/base_toolbar.dart'; class QuillCustomButton extends QuillToolbarBaseButtonOptions { const QuillCustomButton({ diff --git a/lib/src/utils/extensions/build_context.dart b/lib/src/utils/extensions/build_context.dart index 3eadd78a..3c768ae0 100644 --- a/lib/src/utils/extensions/build_context.dart +++ b/lib/src/utils/extensions/build_context.dart @@ -70,7 +70,7 @@ extension BuildContextExt on BuildContext { /// provider widget first and then we will return editor configurations /// throw exception if [QuillProvider] is not in the widget tree QuillEditorConfigurations get requireQuillEditorConfigurations { - return requireQuillConfigurations.editorConfigurations; + return QuillEditorProvider.ofNotNull(this).editorConfigurations; } /// return nullable [QuillEditorConfigurations]. Since the quill @@ -78,7 +78,7 @@ extension BuildContextExt on BuildContext { /// provider widget first and then we will return editor configurations /// don't throw exception if [QuillProvider] is not in the widget tree QuillEditorConfigurations? get quillEditorConfigurations { - return quillConfigurations?.editorConfigurations; + return QuillEditorProvider.of(this)?.editorConfigurations; } /// return [QuillToolbarConfigurations] as not null . Since the quill @@ -86,7 +86,7 @@ extension BuildContextExt on BuildContext { /// provider widget first and then we will return toolbar configurations /// throw exception if [QuillProvider] is not in the widget tree QuillToolbarConfigurations get requireQuillToolbarConfigurations { - return requireQuillConfigurations.toolbarConfigurations; + return QuillToolbarProvider.ofNotNull(this).toolbarConfigurations; } /// return nullable [QuillToolbarConfigurations]. Since the quill @@ -94,7 +94,7 @@ extension BuildContextExt on BuildContext { /// provider widget first and then we will return toolbar configurations /// don't throw exception if [QuillProvider] is not in the widget tree QuillToolbarConfigurations? get quillToolbarConfigurations { - return quillConfigurations?.toolbarConfigurations; + return QuillToolbarProvider.of(this)?.toolbarConfigurations; } /// return nullable [QuillToolbarBaseButtonOptions]. Since the quill diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index f5273e12..73a89fcb 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -41,6 +41,18 @@ bool isMacOS([TargetPlatform? targetPlatform]) { return TargetPlatform.macOS == targetPlatform; } +bool isIOS([TargetPlatform? targetPlatform]) { + if (isWeb()) return false; + targetPlatform ??= defaultTargetPlatform; + return TargetPlatform.iOS == targetPlatform; +} + +bool isAndroid([TargetPlatform? targetPlatform]) { + if (isWeb()) return false; + targetPlatform ??= defaultTargetPlatform; + return TargetPlatform.android == targetPlatform; +} + bool isFlutterTest() { if (isWeb()) return false; return Platform.environment.containsKey('FLUTTER_TEST'); diff --git a/lib/src/widgets/editor/editor.dart b/lib/src/widgets/editor/editor.dart index 9502d229..4bd43e1c 100644 --- a/lib/src/widgets/editor/editor.dart +++ b/lib/src/widgets/editor/editor.dart @@ -1,7 +1,9 @@ import 'dart:math' as math; // ignore: unnecessary_import -import 'dart:typed_data'; +// import 'dart:typed_data'; +// The project maanged to compiled successfully without the import +// do we still need this import (dart:typed_data)?? import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -19,7 +21,6 @@ import '../box.dart'; import '../cursor.dart'; import '../delegate.dart'; import '../float_cursor.dart'; -import '../link.dart'; import '../raw_editor/raw_editor.dart'; import '../text_selection.dart'; @@ -143,53 +144,16 @@ abstract class RenderAbstractEditor implements TextLayoutMetrics { class QuillEditor extends StatefulWidget { const QuillEditor({ + required this.configurations, required this.focusNode, required this.scrollController, - required this.scrollable, - required this.padding, - required this.autoFocus, - required this.expands, - this.textSelectionThemeData, - this.showCursor, - this.paintCursorAboveText, - this.enableInteractiveSelection = true, - this.enableSelectionToolbar = true, - this.scrollBottomInset = 0, - this.minHeight, - this.maxHeight, - this.maxContentWidth, - this.customStyles, - this.textCapitalization = TextCapitalization.sentences, - this.keyboardAppearance = Brightness.light, - this.scrollPhysics, - this.onLaunchUrl, - this.onTapDown, - this.onTapUp, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.embedBuilders, - this.unknownEmbedBuilder, - this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, - this.customStyleBuilder, - this.customRecognizerBuilder, - this.floatingCursorDisabled = false, - this.textSelectionControls, - this.onImagePaste, - this.customShortcuts, - this.customActions, - this.detectWordBoundary = true, - this.enableUnfocusOnTapOutside = true, - this.customLinkPrefixes = const [], - this.dialogTheme, - this.contentInsertionConfiguration, - this.contextMenuBuilder, - this.editorKey, - Key? key, - }) : super(key: key); + super.key, + }); factory QuillEditor.basic({ - required bool readOnly, + /// The configurations for the quill editor widget of flutter quill + QuillEditorConfigurations configurations = + const QuillEditorConfigurations(), TextSelectionThemeData? textSelectionThemeData, Brightness? keyboardAppearance, Iterable? embedBuilders, @@ -197,24 +161,25 @@ class QuillEditor extends StatefulWidget { bool autoFocus = true, bool expands = false, FocusNode? focusNode, - String? placeholder, GlobalKey? editorKey, }) { return QuillEditor( scrollController: ScrollController(), - scrollable: true, focusNode: focusNode ?? FocusNode(), - textSelectionThemeData: textSelectionThemeData, - autoFocus: autoFocus, - expands: expands, - padding: padding, - keyboardAppearance: keyboardAppearance ?? Brightness.light, - embedBuilders: embedBuilders, - editorKey: editorKey, + configurations: configurations.copyWith( + textSelectionThemeData: textSelectionThemeData, + autoFocus: autoFocus, + expands: expands, + padding: padding, + keyboardAppearance: keyboardAppearance ?? Brightness.light, + embedBuilders: embedBuilders, + editorKey: editorKey, + ), ); } - // final QuillController controller; + /// The configurations for the quill editor widget of flutter quill + final QuillEditorConfigurations configurations; /// Controls whether this editor has keyboard focus. final FocusNode focusNode; @@ -222,214 +187,6 @@ class QuillEditor extends StatefulWidget { /// The [ScrollController] to use when vertically scrolling the contents. final ScrollController scrollController; - /// Whether this editor should create a scrollable container for its content. - /// - /// When set to `true` the editor's height can be controlled by [minHeight], - /// [maxHeight] and [expands] properties. - /// - /// When set to `false` the editor always expands to fit the entire content - /// of the document and should normally be placed as a child of another - /// scrollable widget, otherwise the content may be clipped. - final bool scrollable; - final double scrollBottomInset; - - /// Additional space around the content of this editor. - final EdgeInsetsGeometry padding; - - /// Whether this editor should focus itself if nothing else is already - /// focused. - /// - /// If true, the keyboard will open as soon as this editor obtains focus. - /// Otherwise, the keyboard is only shown after the user taps the editor. - /// - /// Defaults to `false`. Cannot be `null`. - final bool autoFocus; - - /// Whether focus should be revoked on tap outside the editor. - final bool enableUnfocusOnTapOutside; - - /// Whether to show cursor. - /// - /// The cursor refers to the blinking caret when the editor is focused. - final bool? showCursor; - final bool? paintCursorAboveText; - - /// Whether to enable user interface affordances for changing the - /// text selection. - /// - /// For example, setting this to true will enable features such as - /// long-pressing the editor to select text and show the - /// cut/copy/paste menu, and tapping to move the text cursor. - /// - /// When this is false, the text selection cannot be adjusted by - /// the user, text cannot be copied, and the user cannot paste into - /// the text field from the clipboard. - /// - /// To disable just the selection toolbar, set enableSelectionToolbar - /// to false. - final bool enableInteractiveSelection; - - /// Whether to show the cut/copy/paste menu when selecting text. - final bool enableSelectionToolbar; - - /// The minimum height to be occupied by this editor. - /// - /// This only has effect if [scrollable] is set to `true` and [expands] is - /// set to `false`. - final double? minHeight; - - /// The maximum height to be occupied by this editor. - /// - /// This only has effect if [scrollable] is set to `true` and [expands] is - /// set to `false`. - final double? maxHeight; - - /// The maximum width to be occupied by the content of this editor. - /// - /// If this is not null and and this editor's width is larger than this value - /// then the contents will be constrained to the provided maximum width and - /// horizontally centered. This is mostly useful on devices with wide screens. - final double? maxContentWidth; - - /// Allows to override [DefaultStyles]. - final DefaultStyles? customStyles; - - /// Whether this editor's height will be sized to fill its parent. - /// - /// This only has effect if [scrollable] is set to `true`. - /// - /// If expands is set to true and wrapped in a parent widget like [Expanded] - /// or [SizedBox], the editor will expand to fill the parent. - /// - /// [maxHeight] and [minHeight] must both be `null` when this is set to - /// `true`. - /// - /// Defaults to `false`. - final bool expands; - - /// Configures how the platform keyboard will select an uppercase or - /// lowercase keyboard. - /// - /// Only supports text keyboards, other keyboard types will ignore this - /// configuration. Capitalization is locale-aware. - /// - /// Defaults to [TextCapitalization.sentences]. Must not be `null`. - final TextCapitalization textCapitalization; - - /// The appearance of the keyboard. - /// - /// This setting is only honored on iOS devices. - /// - /// Defaults to [Brightness.light]. - final Brightness keyboardAppearance; - - /// The [ScrollPhysics] to use when vertically scrolling the input. - /// - /// This only has effect if [scrollable] is set to `true`. - /// - /// If not specified, it will behave according to the current platform. - /// - /// See [Scrollable.physics]. - final ScrollPhysics? scrollPhysics; - - /// Callback to invoke when user wants to launch a URL. - final ValueChanged? onLaunchUrl; - - // Returns whether gesture is handled - final bool Function( - TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; - - // Returns whether gesture is handled - final bool Function( - TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; - - // Returns whether gesture is handled - final bool Function( - LongPressStartDetails details, TextPosition Function(Offset offset))? - onSingleLongTapStart; - - // Returns whether gesture is handled - final bool Function(LongPressMoveUpdateDetails details, - TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; - - // Returns whether gesture is handled - final bool Function( - LongPressEndDetails details, TextPosition Function(Offset offset))? - onSingleLongTapEnd; - - final Iterable? embedBuilders; - final EmbedBuilder? unknownEmbedBuilder; - final CustomStyleBuilder? customStyleBuilder; - final CustomRecognizerBuilder? customRecognizerBuilder; - - /// Delegate function responsible for showing menu with link actions on - /// mobile platforms (iOS, Android). - /// - /// The menu is triggered in editing mode ([readOnly] is set to `false`) - /// when the user long-presses a link-styled text segment. - /// - /// FlutterQuill provides default implementation which can be overridden by - /// this field to customize the user experience. - /// - /// By default on iOS the menu is displayed with [showCupertinoModalPopup] - /// which constructs an instance of [CupertinoActionSheet]. For Android, - /// the menu is displayed with [showModalBottomSheet] and a list of - /// Material [ListTile]s. - final LinkActionPickerDelegate linkActionPickerDelegate; - - final bool floatingCursorDisabled; - - /// allows to create a custom textSelectionControls, - /// if this is null a default textSelectionControls based on the app's theme - /// will be used - final TextSelectionControls? textSelectionControls; - - /// Callback when the user pastes the given image. - /// - /// Returns the url of the image if the image should be inserted. - final Future Function(Uint8List imageBytes)? onImagePaste; - - /// Contains user-defined shortcuts map. - /// - /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] - final Map? customShortcuts; - - /// Contains user-defined actions. - /// - /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] - final Map>? customActions; - - final bool detectWordBoundary; - - /// Additional list if links prefixes, which must not be prepended - /// with "https://" when [LinkMenuAction.launch] happened - /// - /// Useful for deeplinks - final List customLinkPrefixes; - - /// Configures the dialog theme. - final QuillDialogTheme? dialogTheme; - - // Allows for creating a custom context menu - final QuillEditorContextMenuBuilder? contextMenuBuilder; - - /// Configuration of handler for media content inserted via the system input - /// method. - /// - /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] - final ContentInsertionConfiguration? contentInsertionConfiguration; - - /// Using the editorKey for get getLocalRectForCaret - /// editorKey.currentState?.renderEditor.getLocalRectForCaret - final GlobalKey? editorKey; - - /// By default we will use - /// ``` - /// TextSelectionTheme.of(context) - /// ``` - /// to change it please pass a different value - final TextSelectionThemeData? textSelectionThemeData; - @override QuillEditorState createState() => QuillEditorState(); } @@ -440,20 +197,26 @@ class QuillEditorState extends State late EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + QuillEditorConfigurations get configurations { + return widget.configurations; + } + @override void initState() { super.initState(); - _editorKey = widget.editorKey ?? GlobalKey(); + _editorKey = configurations.editorKey ?? GlobalKey(); _selectionGestureDetectorBuilder = _QuillEditorSelectionGestureDetectorBuilder( - this, widget.detectWordBoundary); + this, + configurations.detectWordBoundary, + ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final selectionTheme = - widget.textSelectionThemeData ?? TextSelectionTheme.of(context); + configurations.textSelectionThemeData ?? TextSelectionTheme.of(context); TextSelectionControls textSelectionControls; bool paintCursorAboveText; @@ -483,61 +246,65 @@ class QuillEditorState extends State theme.colorScheme.primary.withOpacity(0.40); } - final showSelectionToolbar = - widget.enableInteractiveSelection && widget.enableSelectionToolbar; - - final editorConfigurations = - context.requireQuillConfigurations.editorConfigurations; - - final child = RawEditor( - key: _editorKey, - controller: context.requireQuillController, - focusNode: widget.focusNode, - scrollController: widget.scrollController, - scrollable: widget.scrollable, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - readOnly: editorConfigurations.readOnly, - placeholder: editorConfigurations.placeholder, - onLaunchUrl: widget.onLaunchUrl, - contextMenuBuilder: showSelectionToolbar - ? (widget.contextMenuBuilder ?? RawEditor.defaultContextMenuBuilder) - : null, - showSelectionHandles: isMobile(theme.platform), - showCursor: widget.showCursor, - cursorStyle: CursorStyle( - color: cursorColor, - backgroundColor: Colors.grey, - width: 2, - radius: cursorRadius, - offset: cursorOffset, - paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, - opacityAnimates: cursorOpacityAnimates, + final showSelectionToolbar = configurations.enableInteractiveSelection && + configurations.enableSelectionToolbar; + + final child = QuillEditorProvider( + editorConfigurations: configurations, + child: RawEditor( + key: _editorKey, + controller: context.requireQuillController, + focusNode: widget.focusNode, + scrollController: widget.scrollController, + scrollable: configurations.scrollable, + scrollBottomInset: configurations.scrollBottomInset, + padding: configurations.padding, + readOnly: configurations.readOnly, + placeholder: configurations.placeholder, + onLaunchUrl: configurations.onLaunchUrl, + contextMenuBuilder: showSelectionToolbar + ? (configurations.contextMenuBuilder ?? + RawEditor.defaultContextMenuBuilder) + : null, + showSelectionHandles: isMobile(theme.platform), + showCursor: configurations.showCursor, + cursorStyle: CursorStyle( + color: cursorColor, + backgroundColor: Colors.grey, + width: 2, + radius: cursorRadius, + offset: cursorOffset, + paintAboveText: + configurations.paintCursorAboveText ?? paintCursorAboveText, + opacityAnimates: cursorOpacityAnimates, + ), + textCapitalization: configurations.textCapitalization, + minHeight: configurations.minHeight, + maxHeight: configurations.maxHeight, + maxContentWidth: configurations.maxContentWidth, + customStyles: configurations.customStyles, + expands: configurations.expands, + autoFocus: configurations.autoFocus, + selectionColor: selectionColor, + selectionCtrls: + configurations.textSelectionControls ?? textSelectionControls, + keyboardAppearance: configurations.keyboardAppearance, + enableInteractiveSelection: configurations.enableInteractiveSelection, + scrollPhysics: configurations.scrollPhysics, + embedBuilder: _getEmbedBuilder, + linkActionPickerDelegate: configurations.linkActionPickerDelegate, + customStyleBuilder: configurations.customStyleBuilder, + customRecognizerBuilder: configurations.customRecognizerBuilder, + floatingCursorDisabled: configurations.floatingCursorDisabled, + onImagePaste: configurations.onImagePaste, + customShortcuts: configurations.customShortcuts, + customActions: configurations.customActions, + customLinkPrefixes: configurations.customLinkPrefixes, + enableUnfocusOnTapOutside: configurations.enableUnfocusOnTapOutside, + dialogTheme: configurations.dialogTheme, + contentInsertionConfiguration: + configurations.contentInsertionConfiguration, ), - textCapitalization: widget.textCapitalization, - minHeight: widget.minHeight, - maxHeight: widget.maxHeight, - maxContentWidth: widget.maxContentWidth, - customStyles: widget.customStyles, - expands: widget.expands, - autoFocus: widget.autoFocus, - selectionColor: selectionColor, - selectionCtrls: widget.textSelectionControls ?? textSelectionControls, - keyboardAppearance: widget.keyboardAppearance, - enableInteractiveSelection: widget.enableInteractiveSelection, - scrollPhysics: widget.scrollPhysics, - embedBuilder: _getEmbedBuilder, - linkActionPickerDelegate: widget.linkActionPickerDelegate, - customStyleBuilder: widget.customStyleBuilder, - customRecognizerBuilder: widget.customRecognizerBuilder, - floatingCursorDisabled: widget.floatingCursorDisabled, - onImagePaste: widget.onImagePaste, - customShortcuts: widget.customShortcuts, - customActions: widget.customActions, - customLinkPrefixes: widget.customLinkPrefixes, - enableUnfocusOnTapOutside: widget.enableUnfocusOnTapOutside, - dialogTheme: widget.dialogTheme, - contentInsertionConfiguration: widget.contentInsertionConfiguration, ); final editor = I18n( @@ -545,7 +312,7 @@ class QuillEditorState extends State child: selectionEnabled ? _selectionGestureDetectorBuilder.build( behavior: HitTestBehavior.translucent, - detectWordBoundary: widget.detectWordBoundary, + detectWordBoundary: configurations.detectWordBoundary, child: child, ) : child, @@ -569,7 +336,7 @@ class QuillEditorState extends State } EmbedBuilder _getEmbedBuilder(Embed node) { - final builders = widget.embedBuilders; + final builders = configurations.embedBuilders; if (builders != null) { for (final builder in builders) { @@ -579,8 +346,9 @@ class QuillEditorState extends State } } - if (widget.unknownEmbedBuilder != null) { - return widget.unknownEmbedBuilder!; + final unknownEmbedBuilder = configurations.unknownEmbedBuilder; + if (unknownEmbedBuilder != null) { + return unknownEmbedBuilder; } throw UnimplementedError( @@ -598,7 +366,7 @@ class QuillEditorState extends State bool get forcePressEnabled => false; @override - bool get selectionEnabled => widget.enableInteractiveSelection; + bool get selectionEnabled => configurations.enableInteractiveSelection; void _requestKeyboard() { final editorCurrentState = _editorKey.currentState; @@ -633,10 +401,12 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (_state.widget.onSingleLongTapMoveUpdate != null) { + if (_state.configurations.onSingleLongTapMoveUpdate != null) { if (renderEditor != null && - _state.widget.onSingleLongTapMoveUpdate!( - details, renderEditor!.getPositionForOffset)) { + _state.configurations.onSingleLongTapMoveUpdate!( + details, + renderEditor!.getPositionForOffset, + )) { return; } } @@ -683,10 +453,12 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onTapDown(TapDownDetails details) { - if (_state.widget.onTapDown != null) { + if (_state.configurations.onTapDown != null) { if (renderEditor != null && - _state.widget.onTapDown!( - details, renderEditor!.getPositionForOffset)) { + _state.configurations.onTapDown!( + details, + renderEditor!.getPositionForOffset, + )) { return; } } @@ -702,9 +474,12 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleTapUp(TapUpDetails details) { - if (_state.widget.onTapUp != null && + if (_state.configurations.onTapUp != null && renderEditor != null && - _state.widget.onTapUp!(details, renderEditor!.getPositionForOffset)) { + _state.configurations.onTapUp!( + details, + renderEditor!.getPositionForOffset, + )) { return; } @@ -765,10 +540,12 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapStart(LongPressStartDetails details) { - if (_state.widget.onSingleLongTapStart != null) { + if (_state.configurations.onSingleLongTapStart != null) { if (renderEditor != null && - _state.widget.onSingleLongTapStart!( - details, renderEditor!.getPositionForOffset)) { + _state.configurations.onSingleLongTapStart!( + details, + renderEditor!.getPositionForOffset, + )) { return; } } @@ -789,10 +566,12 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapEnd(LongPressEndDetails details) { - if (_state.widget.onSingleLongTapEnd != null) { + if (_state.configurations.onSingleLongTapEnd != null) { if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!( - details, renderEditor!.getPositionForOffset)) { + if (_state.configurations.onSingleLongTapEnd!( + details, + renderEditor!.getPositionForOffset, + )) { return; } @@ -1447,6 +1226,28 @@ class RenderEditor extends RenderEditableContainerBox @override Rect getLocalRectForCaret(TextPosition position) { final targetChild = childAtPosition(position); + // TODO: There is a bug here + // The provided text position is not in the current node + // 'package:flutter_quill/src/widgets/text_block.dart': + // text_block.dart:1 + // Failed assertion: line 604 pos 12: + // 'container.containsOffset(position.offset)' + // When the exception was thrown, this was the stack + // #2 RenderEditableTextBlock.globalToLocalPosition + // text_block.dart:604 + // #3 RenderEditor.getLocalRectForCaret + // editor.dart:1230 + // #4 RawEditorStateTextInputClientMixin._updateComposingRectIfNeeded + // raw_editor_state_text_input_client_mixin.dart:85 + // #5 RawEditorStateTextInputClientMixin.openConnectionIfNeeded + // raw_editor_state_text_input_client_mixin.dart:70 + // #6 RawEditorState.requestKeyboard + // raw_editor.dart:1428 + // #7 QuillEditorState._requestKeyboard + // editor.dart:379 + // #8 _QuillEditorSelectionGestureDetectorBuilder.onSingleTapUp + // editor.dart:538 + // #9 _EditorTextSelectionGestureDetectorState._handleTapUp final localPosition = targetChild.globalToLocalPosition(position); final childLocalRect = targetChild.getLocalRectForCaret(localPosition); @@ -1652,8 +1453,9 @@ class RenderEditor extends RenderEditableContainerBox @override TextPosition getTextPositionBelow(TextPosition position) { final child = childAtPosition(position); - final localPosition = - TextPosition(offset: position.offset - child.container.documentOffset); + final localPosition = TextPosition( + offset: position.offset - child.container.documentOffset, + ); var newPosition = child.getPositionBelow(localPosition); @@ -1672,11 +1474,13 @@ class RenderEditor extends RenderEditableContainerBox final finalOffset = Offset(caretOffset.dx, testOffset.dy); final siblingPosition = sibling.getPositionForOffset(finalOffset); newPosition = TextPosition( - offset: sibling.container.documentOffset + siblingPosition.offset); + offset: sibling.container.documentOffset + siblingPosition.offset, + ); } } else { newPosition = TextPosition( - offset: child.container.documentOffset + newPosition.offset); + offset: child.container.documentOffset + newPosition.offset, + ); } return newPosition; } diff --git a/lib/src/widgets/link.dart b/lib/src/widgets/link.dart index 3271abae..345a0027 100644 --- a/lib/src/widgets/link.dart +++ b/lib/src/widgets/link.dart @@ -53,9 +53,11 @@ Future defaultLinkActionPickerDelegate( return _showMaterialMenu(context, link); default: assert( - false, - 'defaultShowLinkActionsMenu not supposed to ' - 'be invoked for $defaultTargetPlatform'); + false, + 'defaultShowLinkActionsMenu not supposed to ' + 'be invoked for $defaultTargetPlatform. ' + "it's only supported for iOS and Android.", + ); return LinkMenuAction.none; } } diff --git a/lib/src/widgets/raw_editor/raw_editor.dart b/lib/src/widgets/raw_editor/raw_editor.dart index 613e72b0..c3efe93c 100644 --- a/lib/src/widgets/raw_editor/raw_editor.dart +++ b/lib/src/widgets/raw_editor/raw_editor.dart @@ -36,6 +36,7 @@ import '../../models/themes/quill_dialog_theme.dart'; import '../../utils/cast.dart'; import '../../utils/delta.dart'; import '../../utils/embeds.dart'; +import '../../utils/extensions/build_context.dart'; import '../../utils/platform.dart'; import '../controller.dart'; import '../cursor.dart'; @@ -880,6 +881,9 @@ class RawEditorState extends EditorState /// Updates the checkbox positioned at [offset] in document /// by changing its attribute according to [value]. void _handleCheckboxTap(int offset, bool value) { + final requestKeyboardFocusOnCheckListChanged = context + .requireQuillEditorConfigurations + .requestKeyboardFocusOnCheckListChanged; if (!widget.readOnly) { _disableScrollControllerAnimateOnce = true; final currentSelection = controller.selection.copyWith(); @@ -888,6 +892,7 @@ class RawEditorState extends EditorState _markNeedsBuild(); controller ..ignoreFocusOnTextChange = true + ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged ..formatText(offset, 0, attribute) // Checkbox tapping causes controller.selection to go to offset 0 @@ -901,6 +906,7 @@ class RawEditorState extends EditorState SchedulerBinding.instance.addPostFrameCallback((_) { controller ..ignoreFocusOnTextChange = false + ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged ..updateSelection(currentSelection, ChangeSource.LOCAL); }); } diff --git a/lib/src/widgets/style_widgets/checkbox_point.dart b/lib/src/widgets/style_widgets/checkbox_point.dart index 905f00b3..1276a152 100644 --- a/lib/src/widgets/style_widgets/checkbox_point.dart +++ b/lib/src/widgets/style_widgets/checkbox_point.dart @@ -10,8 +10,8 @@ class CheckboxPoint extends StatefulWidget { required this.enabled, required this.onChanged, this.uiBuilder, - Key? key, - }) : super(key: key); + super.key, + }); final double size; final bool value; @@ -26,8 +26,9 @@ class CheckboxPoint extends StatefulWidget { class _CheckboxPointState extends State { @override Widget build(BuildContext context) { - if (widget.uiBuilder != null) { - return widget.uiBuilder!.build( + final uiBuilder = widget.uiBuilder; + if (uiBuilder != null) { + return uiBuilder.build( context: context, isChecked: widget.value, onChanged: widget.onChanged, diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 4a623d9b..786d8849 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -52,29 +52,30 @@ const List romanNumbers = [ ]; class EditableTextBlock extends StatelessWidget { - const EditableTextBlock( - {required this.block, - required this.controller, - required this.textDirection, - required this.scrollBottomInset, - required this.verticalSpacing, - required this.textSelection, - required this.color, - required this.styles, - required this.enableInteractiveSelection, - required this.hasFocus, - required this.contentPadding, - required this.embedBuilder, - required this.linkActionPicker, - required this.cursorCont, - required this.indentLevelCounts, - required this.clearIndents, - required this.onCheckboxTap, - required this.readOnly, - this.onLaunchUrl, - this.customStyleBuilder, - this.customLinkPrefixes = const [], - Key? key}); + const EditableTextBlock({ + required this.block, + required this.controller, + required this.textDirection, + required this.scrollBottomInset, + required this.verticalSpacing, + required this.textSelection, + required this.color, + required this.styles, + required this.enableInteractiveSelection, + required this.hasFocus, + required this.contentPadding, + required this.embedBuilder, + required this.linkActionPicker, + required this.cursorCont, + required this.indentLevelCounts, + required this.clearIndents, + required this.onCheckboxTap, + required this.readOnly, + this.onLaunchUrl, + this.customStyleBuilder, + this.customLinkPrefixes = const [], + super.key, + }); final Block block; final QuillController controller; diff --git a/lib/src/widgets/toolbar/base_toolbar.dart b/lib/src/widgets/toolbar/base_toolbar.dart new file mode 100644 index 00000000..5b75dfe3 --- /dev/null +++ b/lib/src/widgets/toolbar/base_toolbar.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:i18n_extension/i18n_widget.dart'; + +import '../../../flutter_quill.dart'; +import '../../models/config/toolbar/base_configurations.dart'; +import '../../utils/extensions/build_context.dart'; +import 'buttons/arrow_indicated_list.dart'; + +export '../../models/config/toolbar/buttons/base.dart'; +export '../../models/config/toolbar/configurations.dart'; +export 'buttons/clear_format.dart'; +export 'buttons/color.dart'; +export 'buttons/custom_button.dart'; +export 'buttons/font_family.dart'; +export 'buttons/font_size.dart'; +export 'buttons/history.dart'; +export 'buttons/indent.dart'; +export 'buttons/link_style.dart'; +export 'buttons/link_style2.dart'; +export 'buttons/quill_icon.dart'; +export 'buttons/search/search.dart'; +export 'buttons/select_alignment.dart'; +export 'buttons/select_header_style.dart'; +export 'buttons/toggle_check_list.dart'; +export 'buttons/toggle_style.dart'; + +typedef QuillBaseToolbarChildrenBuilder = List Function( + BuildContext context, +); + +class QuillBaseToolbar extends StatelessWidget implements PreferredSizeWidget { + const QuillBaseToolbar({ + required this.configurations, + super.key, + }); + + final QuillBaseToolbarConfigurations configurations; + + // We can't get the modified [toolbarSize] by the developer + // but we tested the [QuillToolbar] on the [appBar] and I didn't notice + // a difference no matter what the value is so I will leave it to the + // default + @override + Size get preferredSize => configurations.axis == Axis.horizontal + ? const Size.fromHeight(defaultToolbarSize) + : const Size.fromWidth(defaultToolbarSize); + + @override + Widget build(BuildContext context) { + final toolbarSize = configurations.toolbarSize; + return I18n( + initialLocale: context.quillSharedConfigurations?.locale, + child: Builder( + builder: (context) { + if (configurations.multiRowsDisplay) { + return Wrap( + direction: configurations.axis, + alignment: configurations.toolbarIconAlignment, + crossAxisAlignment: configurations.toolbarIconCrossAlignment, + runSpacing: 4, + spacing: configurations.toolbarSectionSpacing, + children: configurations.childrenBuilder(context), + ); + } + return Container( + decoration: configurations.decoration ?? + BoxDecoration( + color: configurations.color ?? Theme.of(context).canvasColor, + ), + constraints: BoxConstraints.tightFor( + height: + configurations.axis == Axis.horizontal ? toolbarSize : null, + width: configurations.axis == Axis.vertical ? toolbarSize : null, + ), + child: QuillToolbarArrowIndicatedButtonList( + axis: configurations.axis, + buttons: configurations.childrenBuilder(context), + ), + ); + }, + ), + ); + } +} + +/// The divider which is used for separation of buttons in the toolbar. +/// +/// It can be used outside of this package, for example when user does not use +/// [QuillBaseToolbar.basic] and compose toolbar's children on its own. +class QuillToolbarDivider extends StatelessWidget { + const QuillToolbarDivider( + this.axis, { + super.key, + this.color, + this.space, + }); + + /// Provides a horizontal divider for vertical toolbar. + const QuillToolbarDivider.horizontal({Color? color, double? space}) + : this(Axis.horizontal, color: color, space: space); + + /// Provides a horizontal divider for horizontal toolbar. + const QuillToolbarDivider.vertical({Color? color, double? space}) + : this(Axis.vertical, color: color, space: space); + + /// The axis along which the toolbar is. + final Axis axis; + + /// The color to use when painting this divider's line. + final Color? color; + + /// The divider's space (width or height) depending of [axis]. + final double? space; + + @override + Widget build(BuildContext context) { + // Vertical toolbar requires horizontal divider, and vice versa + return axis == Axis.vertical + ? Divider( + height: space, + color: color, + indent: 12, + endIndent: 12, + ) + : VerticalDivider( + width: space, + color: color, + indent: 12, + endIndent: 12, + ); + } +} diff --git a/lib/src/widgets/toolbar/buttons/clear_format.dart b/lib/src/widgets/toolbar/buttons/clear_format.dart index 97a7994f..aabf5576 100644 --- a/lib/src/widgets/toolbar/buttons/clear_format.dart +++ b/lib/src/widgets/toolbar/buttons/clear_format.dart @@ -5,7 +5,7 @@ import '../../../models/documents/attribute.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class QuillToolbarClearFormatButton extends StatelessWidget { const QuillToolbarClearFormatButton({ diff --git a/lib/src/widgets/toolbar/buttons/color.dart b/lib/src/widgets/toolbar/buttons/color.dart index 69671c13..fd5ae817 100644 --- a/lib/src/widgets/toolbar/buttons/color.dart +++ b/lib/src/widgets/toolbar/buttons/color.dart @@ -8,7 +8,7 @@ import '../../../translations/toolbar.i18n.dart'; import '../../../utils/color.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; /// Controls color styles. /// diff --git a/lib/src/widgets/toolbar/buttons/custom_button.dart b/lib/src/widgets/toolbar/buttons/custom_button.dart index b958ed0e..14911446 100644 --- a/lib/src/widgets/toolbar/buttons/custom_button.dart +++ b/lib/src/widgets/toolbar/buttons/custom_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class CustomButton extends StatelessWidget { const CustomButton({ diff --git a/lib/src/widgets/toolbar/buttons/history.dart b/lib/src/widgets/toolbar/buttons/history.dart index f4bb102b..2098eb52 100644 --- a/lib/src/widgets/toolbar/buttons/history.dart +++ b/lib/src/widgets/toolbar/buttons/history.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../../../translations.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class QuillToolbarHistoryButton extends StatefulWidget { const QuillToolbarHistoryButton({ diff --git a/lib/src/widgets/toolbar/buttons/indent.dart b/lib/src/widgets/toolbar/buttons/indent.dart index c22c2c1f..0ce83764 100644 --- a/lib/src/widgets/toolbar/buttons/indent.dart +++ b/lib/src/widgets/toolbar/buttons/indent.dart @@ -5,7 +5,11 @@ import '../../../models/themes/quill_icon_theme.dart'; import '../../../translations/toolbar.i18n.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart' + show + QuillToolbarBaseButtonOptions, + QuillToolbarIconButton, + kIconButtonFactor; class QuillToolbarIndentButton extends StatefulWidget { const QuillToolbarIndentButton({ diff --git a/lib/src/widgets/toolbar/buttons/link_style.dart b/lib/src/widgets/toolbar/buttons/link_style.dart index efeadf73..2188aeff 100644 --- a/lib/src/widgets/toolbar/buttons/link_style.dart +++ b/lib/src/widgets/toolbar/buttons/link_style.dart @@ -9,7 +9,7 @@ import '../../../translations/toolbar.i18n.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../../link.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class QuillToolbarLinkStyleButton extends StatefulWidget { const QuillToolbarLinkStyleButton({ diff --git a/lib/src/widgets/toolbar/buttons/link_style2.dart b/lib/src/widgets/toolbar/buttons/link_style2.dart index 0fea44ce..d2ecbddd 100644 --- a/lib/src/widgets/toolbar/buttons/link_style2.dart +++ b/lib/src/widgets/toolbar/buttons/link_style2.dart @@ -10,7 +10,7 @@ import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../controller.dart'; import '../../link.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; /// Alternative version of [QuillToolbarLinkStyleButton]. This widget has more /// customization diff --git a/lib/src/widgets/toolbar/buttons/search/search.dart b/lib/src/widgets/toolbar/buttons/search/search.dart index 3ff46a9f..6773aab5 100644 --- a/lib/src/widgets/toolbar/buttons/search/search.dart +++ b/lib/src/widgets/toolbar/buttons/search/search.dart @@ -5,7 +5,7 @@ import '../../../../models/themes/quill_dialog_theme.dart'; import '../../../../models/themes/quill_icon_theme.dart'; import '../../../../utils/extensions/build_context.dart'; import '../../../controller.dart'; -import '../../toolbar.dart'; +import '../../base_toolbar.dart'; class QuillToolbarSearchButton extends StatelessWidget { const QuillToolbarSearchButton({ diff --git a/lib/src/widgets/toolbar/buttons/select_alignment.dart b/lib/src/widgets/toolbar/buttons/select_alignment.dart index 4a5fe0d0..03bccd82 100644 --- a/lib/src/widgets/toolbar/buttons/select_alignment.dart +++ b/lib/src/widgets/toolbar/buttons/select_alignment.dart @@ -8,7 +8,7 @@ import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/extensions/build_context.dart'; import '../../../utils/widgets.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class QuillToolbarSelectAlignmentButton extends StatefulWidget { const QuillToolbarSelectAlignmentButton({ diff --git a/lib/src/widgets/toolbar/buttons/select_header_style.dart b/lib/src/widgets/toolbar/buttons/select_header_style.dart index 5ce9270b..7b840e11 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style.dart @@ -7,7 +7,7 @@ import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; class QuillToolbarSelectHeaderStyleButtons extends StatefulWidget { const QuillToolbarSelectHeaderStyleButtons({ diff --git a/lib/src/widgets/toolbar/buttons/toggle_style.dart b/lib/src/widgets/toolbar/buttons/toggle_style.dart index 192dbb93..fa351292 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_style.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_style.dart @@ -7,7 +7,7 @@ import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/extensions/build_context.dart'; import '../../../utils/widgets.dart'; import '../../controller.dart'; -import '../toolbar.dart'; +import '../base_toolbar.dart'; typedef ToggleStyleButtonBuilder = Widget Function( BuildContext context, diff --git a/lib/src/widgets/toolbar/toolbar.dart b/lib/src/widgets/toolbar/toolbar.dart index b88da169..1ea5a15c 100644 --- a/lib/src/widgets/toolbar/toolbar.dart +++ b/lib/src/widgets/toolbar/toolbar.dart @@ -1,584 +1,370 @@ import 'package:flutter/material.dart'; -import 'package:i18n_extension/i18n_widget.dart'; import '../../../flutter_quill.dart'; +import '../../models/config/toolbar/base_configurations.dart'; import '../../utils/extensions/build_context.dart'; -import 'buttons/arrow_indicated_list.dart'; -export '../../models/config/toolbar/buttons/base.dart'; -export '../../models/config/toolbar/configurations.dart'; -export 'buttons/clear_format.dart'; -export 'buttons/color.dart'; -export 'buttons/custom_button.dart'; -export 'buttons/font_family.dart'; -export 'buttons/font_size.dart'; -export 'buttons/history.dart'; -export 'buttons/indent.dart'; -export 'buttons/link_style.dart'; -export 'buttons/link_style2.dart'; -export 'buttons/quill_icon.dart'; -export 'buttons/search/search.dart'; -export 'buttons/select_alignment.dart'; -export 'buttons/select_header_style.dart'; -export 'buttons/toggle_check_list.dart'; -export 'buttons/toggle_style.dart'; - -typedef QuillToolbarChildrenBuilder = List Function( - BuildContext context, -); - -class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { +class QuillToolbar extends StatelessWidget { const QuillToolbar({ - required this.childrenBuilder, - this.axis = Axis.horizontal, - // this.toolbarSize = kDefaultIconSize * 2, - this.toolbarSectionSpacing = kToolbarSectionSpacing, - this.toolbarIconAlignment = WrapAlignment.center, - this.toolbarIconCrossAlignment = WrapCrossAlignment.center, - this.color, - this.customButtons = const [], - this.sectionDividerColor, - this.sectionDividerSpace, - this.linkDialogAction, - this.decoration, - Key? key, - }) : super(key: key); - - factory QuillToolbar.basic({ - double toolbarSectionSpacing = kToolbarSectionSpacing, - WrapAlignment toolbarIconAlignment = WrapAlignment.center, - WrapCrossAlignment toolbarIconCrossAlignment = WrapCrossAlignment.center, - bool showDividers = true, - bool showFontFamily = true, - bool showFontSize = true, - bool showBoldButton = true, - bool showItalicButton = true, - bool showSmallButton = false, - bool showUnderLineButton = true, - bool showStrikeThrough = true, - bool showInlineCode = true, - bool showColorButton = true, - bool showBackgroundColorButton = true, - bool showClearFormat = true, - bool showAlignmentButtons = false, - bool showLeftAlignment = true, - bool showCenterAlignment = true, - bool showRightAlignment = true, - bool showJustifyAlignment = true, - bool showHeaderStyle = true, - bool showListNumbers = true, - bool showListBullets = true, - bool showListCheck = true, - bool showCodeBlock = true, - bool showQuote = true, - bool showIndent = true, - bool showLink = true, - bool showUndo = true, - bool showRedo = true, - bool showDirection = false, - bool showSearchButton = true, - bool showSubscript = true, - bool showSuperscript = true, - List customButtons = const [], - - /// The decoration to use for the toolbar. - Decoration? decoration, - - /// Toolbar items to display for controls of embed blocks - List? embedButtons, - - ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme] - QuillIconTheme? iconTheme, - - ///The theme to use for the theming of the [LinkDialog()], - ///shown when embedding an image, for example - QuillDialogTheme? dialogTheme, - - ///Map of tooltips for toolbar buttons - /// - ///The example is: - ///```dart - /// tooltips = { - /// ToolbarButtons.undo: 'Undo', - /// ToolbarButtons.redo: 'Redo', - /// } - /// - ///``` - /// - /// To disable tooltips just pass empty map as well. - @Deprecated('This is deprecated and will no longer used. ' - 'to change the tooltips please pass them in the Quill toolbar button' - ' configurations which exists in in the QuillProvider') - Map? tooltips, - - /// The locale to use for the editor toolbar, defaults to system locale - /// More at https://github.com/singerdmx/flutter-quill#translation - Locale? locale, + super.key, + this.configurations = const QuillToolbarConfigurations(), + }); - /// The color of the toolbar - Color? color, + /// The configurations for the toolbar widget of flutter quill + final QuillToolbarConfigurations configurations; - /// The color of the toolbar section divider - Color? sectionDividerColor, + @override + Widget build(BuildContext context) { + final theEmbedButtons = configurations.embedButtons; - /// The space occupied by toolbar divider - double? sectionDividerSpace, - Key? key, - }) { final isButtonGroupShown = [ - showFontFamily || - showFontSize || - showBoldButton || - showItalicButton || - showSmallButton || - showUnderLineButton || - showStrikeThrough || - showInlineCode || - showColorButton || - showBackgroundColorButton || - showClearFormat || - embedButtons?.isNotEmpty == true, - showLeftAlignment || - showCenterAlignment || - showRightAlignment || - showJustifyAlignment || - showDirection, - showHeaderStyle, - showListNumbers || showListBullets || showListCheck || showCodeBlock, - showQuote || showIndent, - showLink || showSearchButton + configurations.showFontFamily || + configurations.showFontSize || + configurations.showBoldButton || + configurations.showItalicButton || + configurations.showSmallButton || + configurations.showUnderLineButton || + configurations.showStrikeThrough || + configurations.showInlineCode || + configurations.showColorButton || + configurations.showBackgroundColorButton || + configurations.showClearFormat || + theEmbedButtons?.isNotEmpty == true, + configurations.showLeftAlignment || + configurations.showCenterAlignment || + configurations.showRightAlignment || + configurations.showJustifyAlignment || + configurations.showDirection, + configurations.showHeaderStyle, + configurations.showListNumbers || + configurations.showListBullets || + configurations.showListCheck || + configurations.showCodeBlock, + configurations.showQuote || configurations.showIndent, + configurations.showLink || configurations.showSearchButton ]; - return QuillToolbar( - key: key, - color: color, - decoration: decoration, - toolbarSectionSpacing: toolbarSectionSpacing, - toolbarIconAlignment: toolbarIconAlignment, - toolbarIconCrossAlignment: toolbarIconCrossAlignment, - customButtons: customButtons, - childrenBuilder: (context) { - final controller = context.requireQuillController; - - final toolbarConfigurations = context.requireQuillToolbarConfigurations; - - final globalIconSize = - toolbarConfigurations.buttonOptions.base.globalIconSize; - - final axis = toolbarConfigurations.axis; - - if (tooltips != null) { - throw UnsupportedError( - 'This is deprecated and will no longer used. to change ' - 'the tooltips please pass them in the Quill toolbar button' - 'configurations which exists in in the QuillProvider', - ); - } - - return [ - if (showUndo) - QuillToolbarHistoryButton( - options: toolbarConfigurations.buttonOptions.undoHistory, - controller: - toolbarConfigurations.buttonOptions.undoHistory.controller ?? - context.requireQuillController, - ), - if (showRedo) - QuillToolbarHistoryButton( - options: toolbarConfigurations.buttonOptions.redoHistory, - controller: - toolbarConfigurations.buttonOptions.redoHistory.controller ?? - context.requireQuillController, - ), - if (showFontFamily) - QuillToolbarFontFamilyButton( - options: toolbarConfigurations.buttonOptions.fontFamily, - controller: - toolbarConfigurations.buttonOptions.fontFamily.controller ?? - context.requireQuillController, - ), - if (showFontSize) - QuillToolbarFontSizeButton( - options: toolbarConfigurations.buttonOptions.fontSize, - controller: - toolbarConfigurations.buttonOptions.fontFamily.controller ?? - context.requireQuillController, - ), - if (showBoldButton) - QuillToolbarToggleStyleButton( - attribute: Attribute.bold, - options: toolbarConfigurations.buttonOptions.bold, - controller: toolbarConfigurations.buttonOptions.bold.controller ?? - context.requireQuillController, - ), - if (showSubscript) - QuillToolbarToggleStyleButton( - attribute: Attribute.subscript, - options: toolbarConfigurations.buttonOptions.subscript, - controller: - toolbarConfigurations.buttonOptions.subscript.controller ?? - context.requireQuillController, - ), - if (showSuperscript) - QuillToolbarToggleStyleButton( - attribute: Attribute.superscript, - options: toolbarConfigurations.buttonOptions.superscript, - controller: - toolbarConfigurations.buttonOptions.superscript.controller ?? - context.requireQuillController, - ), - if (showItalicButton) - QuillToolbarToggleStyleButton( - attribute: Attribute.italic, - options: toolbarConfigurations.buttonOptions.italic, - controller: - toolbarConfigurations.buttonOptions.italic.controller ?? - context.requireQuillController, - ), - if (showSmallButton) - QuillToolbarToggleStyleButton( - attribute: Attribute.small, - options: toolbarConfigurations.buttonOptions.small, - controller: - toolbarConfigurations.buttonOptions.small.controller ?? - context.requireQuillController, - ), - if (showUnderLineButton) - QuillToolbarToggleStyleButton( - attribute: Attribute.underline, - options: toolbarConfigurations.buttonOptions.underLine, - controller: - toolbarConfigurations.buttonOptions.underLine.controller ?? - context.requireQuillController, - ), - if (showStrikeThrough) - QuillToolbarToggleStyleButton( - attribute: Attribute.strikeThrough, - options: toolbarConfigurations.buttonOptions.strikeThrough, - controller: toolbarConfigurations - .buttonOptions.strikeThrough.controller ?? - context.requireQuillController, - ), - if (showInlineCode) - QuillToolbarToggleStyleButton( - attribute: Attribute.inlineCode, - options: toolbarConfigurations.buttonOptions.inlineCode, - controller: - toolbarConfigurations.buttonOptions.inlineCode.controller ?? - context.requireQuillController, - ), - if (showColorButton) - QuillToolbarColorButton( - controller: controller, - isBackground: false, - options: toolbarConfigurations.buttonOptions.color, - ), - if (showBackgroundColorButton) - QuillToolbarColorButton( - options: toolbarConfigurations.buttonOptions.backgroundColor, - controller: controller, - isBackground: true, - ), - if (showClearFormat) - QuillToolbarClearFormatButton( - controller: controller, - options: toolbarConfigurations.buttonOptions.clearFormat, - ), - if (embedButtons != null) - for (final builder in embedButtons) - builder(controller, globalIconSize, iconTheme, dialogTheme), - if (showDividers && - isButtonGroupShown[0] && - (isButtonGroupShown[1] || - isButtonGroupShown[2] || - isButtonGroupShown[3] || - isButtonGroupShown[4] || - isButtonGroupShown[5])) - QuillToolbarDivider( - axis, - color: sectionDividerColor, - space: sectionDividerSpace, - ), - if (showAlignmentButtons) - QuillToolbarSelectAlignmentButton( - controller: controller, - options: - toolbarConfigurations.buttonOptions.selectAlignmentButtons, - // tooltips: Map.of(buttonTooltips) - // ..removeWhere((key, value) => ![ - // ToolbarButtons.leftAlignment, - // ToolbarButtons.centerAlignment, - // ToolbarButtons.rightAlignment, - // ToolbarButtons.justifyAlignment, - // ].contains(key)), - showLeftAlignment: showLeftAlignment, - showCenterAlignment: showCenterAlignment, - showRightAlignment: showRightAlignment, - showJustifyAlignment: showJustifyAlignment, - ), - if (showDirection) - QuillToolbarToggleStyleButton( - attribute: Attribute.rtl, - options: toolbarConfigurations.buttonOptions.direction, - controller: - toolbarConfigurations.buttonOptions.direction.controller ?? - context.requireQuillController, - ), - if (showDividers && - isButtonGroupShown[1] && - (isButtonGroupShown[2] || - isButtonGroupShown[3] || - isButtonGroupShown[4] || - isButtonGroupShown[5])) - QuillToolbarDivider( - axis, - color: sectionDividerColor, - space: sectionDividerSpace, - ), - if (showHeaderStyle) - QuillToolbarSelectHeaderStyleButtons( - controller: controller, - options: - toolbarConfigurations.buttonOptions.selectHeaderStyleButtons, - // tooltip: buttonTooltips[ToolbarButtons.headerStyle], - // axis: axis, - // iconSize: toolbarIconSize, - // iconTheme: iconTheme, - // afterButtonPressed: afterButtonPressed, - ), - if (showDividers && - showHeaderStyle && - isButtonGroupShown[2] && - (isButtonGroupShown[3] || - isButtonGroupShown[4] || - isButtonGroupShown[5])) - QuillToolbarDivider( - axis, - color: sectionDividerColor, - space: sectionDividerSpace, - ), - if (showListNumbers) - QuillToolbarToggleStyleButton( - attribute: Attribute.ol, - options: toolbarConfigurations.buttonOptions.listNumbers, - controller: - toolbarConfigurations.buttonOptions.listNumbers.controller ?? - context.requireQuillController, - ), - if (showListBullets) - QuillToolbarToggleStyleButton( - attribute: Attribute.ul, - options: toolbarConfigurations.buttonOptions.listBullets, - controller: - toolbarConfigurations.buttonOptions.listBullets.controller ?? - context.requireQuillController, - ), - if (showListCheck) - QuillToolbarToggleCheckListButton( - options: toolbarConfigurations.buttonOptions.toggleCheckList, - controller: toolbarConfigurations - .buttonOptions.toggleCheckList.controller ?? - context.requireQuillController, - ), - if (showCodeBlock) - QuillToolbarToggleStyleButton( - attribute: Attribute.codeBlock, - options: toolbarConfigurations.buttonOptions.codeBlock, - controller: - toolbarConfigurations.buttonOptions.codeBlock.controller ?? + return QuillToolbarProvider( + toolbarConfigurations: configurations, + child: QuillBaseToolbar( + configurations: QuillBaseToolbarConfigurations( + color: configurations.color, + decoration: configurations.decoration, + toolbarSectionSpacing: configurations.toolbarSectionSpacing, + toolbarIconAlignment: configurations.toolbarIconAlignment, + toolbarIconCrossAlignment: configurations.toolbarIconCrossAlignment, + customButtons: configurations.customButtons, + linkDialogAction: configurations.linkDialogAction, + multiRowsDisplay: configurations.multiRowsDisplay, + sectionDividerColor: configurations.sectionDividerColor, + axis: configurations.axis, + sectionDividerSpace: configurations.sectionDividerSpace, + toolbarSize: configurations.toolbarSize, + childrenBuilder: (context) { + final controller = context.requireQuillController; + + final toolbarConfigurations = + context.requireQuillToolbarConfigurations; + + final globalIconSize = + toolbarConfigurations.buttonOptions.base.globalIconSize; + + final axis = toolbarConfigurations.axis; + final globalController = context.requireQuillController; + + return [ + if (configurations.showUndo) + QuillToolbarHistoryButton( + options: toolbarConfigurations.buttonOptions.undoHistory, + controller: toolbarConfigurations + .buttonOptions.undoHistory.controller ?? + globalController, + ), + if (configurations.showRedo) + QuillToolbarHistoryButton( + options: toolbarConfigurations.buttonOptions.redoHistory, + controller: toolbarConfigurations + .buttonOptions.redoHistory.controller ?? + globalController, + ), + if (configurations.showFontFamily) + QuillToolbarFontFamilyButton( + options: toolbarConfigurations.buttonOptions.fontFamily, + controller: toolbarConfigurations + .buttonOptions.fontFamily.controller ?? + globalController, + ), + if (configurations.showFontSize) + QuillToolbarFontSizeButton( + options: toolbarConfigurations.buttonOptions.fontSize, + controller: toolbarConfigurations + .buttonOptions.fontFamily.controller ?? + globalController, + ), + if (configurations.showBoldButton) + QuillToolbarToggleStyleButton( + attribute: Attribute.bold, + options: toolbarConfigurations.buttonOptions.bold, + controller: + toolbarConfigurations.buttonOptions.bold.controller ?? + globalController, + ), + if (configurations.showSubscript) + QuillToolbarToggleStyleButton( + attribute: Attribute.subscript, + options: toolbarConfigurations.buttonOptions.subscript, + controller: toolbarConfigurations + .buttonOptions.subscript.controller ?? + globalController, + ), + if (configurations.showSuperscript) + QuillToolbarToggleStyleButton( + attribute: Attribute.superscript, + options: toolbarConfigurations.buttonOptions.superscript, + controller: toolbarConfigurations + .buttonOptions.superscript.controller ?? + globalController, + ), + if (configurations.showItalicButton) + QuillToolbarToggleStyleButton( + attribute: Attribute.italic, + options: toolbarConfigurations.buttonOptions.italic, + controller: + toolbarConfigurations.buttonOptions.italic.controller ?? + globalController, + ), + if (configurations.showSmallButton) + QuillToolbarToggleStyleButton( + attribute: Attribute.small, + options: toolbarConfigurations.buttonOptions.small, + controller: + toolbarConfigurations.buttonOptions.small.controller ?? + globalController, + ), + if (configurations.showUnderLineButton) + QuillToolbarToggleStyleButton( + attribute: Attribute.underline, + options: toolbarConfigurations.buttonOptions.underLine, + controller: toolbarConfigurations + .buttonOptions.underLine.controller ?? + globalController, + ), + if (configurations.showStrikeThrough) + QuillToolbarToggleStyleButton( + attribute: Attribute.strikeThrough, + options: toolbarConfigurations.buttonOptions.strikeThrough, + controller: toolbarConfigurations + .buttonOptions.strikeThrough.controller ?? + globalController, + ), + if (configurations.showInlineCode) + QuillToolbarToggleStyleButton( + attribute: Attribute.inlineCode, + options: toolbarConfigurations.buttonOptions.inlineCode, + controller: toolbarConfigurations + .buttonOptions.inlineCode.controller ?? + globalController, + ), + if (configurations.showColorButton) + QuillToolbarColorButton( + controller: controller, + isBackground: false, + options: toolbarConfigurations.buttonOptions.color, + ), + if (configurations.showBackgroundColorButton) + QuillToolbarColorButton( + options: toolbarConfigurations.buttonOptions.backgroundColor, + controller: controller, + isBackground: true, + ), + if (configurations.showClearFormat) + QuillToolbarClearFormatButton( + controller: controller, + options: toolbarConfigurations.buttonOptions.clearFormat, + ), + if (theEmbedButtons != null) + for (final builder in theEmbedButtons) + builder(controller, globalIconSize, configurations.iconTheme, + configurations.dialogTheme), + if (configurations.showDividers && + isButtonGroupShown[0] && + (isButtonGroupShown[1] || + isButtonGroupShown[2] || + isButtonGroupShown[3] || + isButtonGroupShown[4] || + isButtonGroupShown[5])) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, + ), + if (configurations.showAlignmentButtons) + QuillToolbarSelectAlignmentButton( + controller: controller, + options: toolbarConfigurations + .buttonOptions.selectAlignmentButtons, + // tooltips: Map.of(buttonTooltips) + // ..removeWhere((key, value) => ![ + // ToolbarButtons.leftAlignment, + // ToolbarButtons.centerAlignment, + // ToolbarButtons.rightAlignment, + // ToolbarButtons.justifyAlignment, + // ].contains(key)), + showLeftAlignment: configurations.showLeftAlignment, + showCenterAlignment: configurations.showCenterAlignment, + showRightAlignment: configurations.showRightAlignment, + showJustifyAlignment: configurations.showJustifyAlignment, + ), + if (configurations.showDirection) + QuillToolbarToggleStyleButton( + attribute: Attribute.rtl, + options: toolbarConfigurations.buttonOptions.direction, + controller: toolbarConfigurations + .buttonOptions.direction.controller ?? context.requireQuillController, - ), - if (showDividers && - isButtonGroupShown[3] && - (isButtonGroupShown[4] || isButtonGroupShown[5])) - QuillToolbarDivider(axis, - color: sectionDividerColor, space: sectionDividerSpace), - if (showQuote) - QuillToolbarToggleStyleButton( - options: toolbarConfigurations.buttonOptions.quote, - controller: - toolbarConfigurations.buttonOptions.quote.controller ?? - context.requireQuillController, - attribute: Attribute.blockQuote, - ), - if (showIndent) - QuillToolbarIndentButton( - controller: toolbarConfigurations - .buttonOptions.indentIncrease.controller ?? - context.requireQuillController, - isIncrease: true, - options: toolbarConfigurations.buttonOptions.indentIncrease, - ), - if (showIndent) - QuillToolbarIndentButton( - controller: toolbarConfigurations - .buttonOptions.indentDecrease.controller ?? - context.requireQuillController, - isIncrease: false, - options: toolbarConfigurations.buttonOptions.indentDecrease, - ), - if (showDividers && isButtonGroupShown[4] && isButtonGroupShown[5]) - QuillToolbarDivider(axis, - color: sectionDividerColor, space: sectionDividerSpace), - if (showLink) - QuillToolbarLinkStyleButton( - controller: controller, - options: toolbarConfigurations.buttonOptions.linkStyle, - ), - if (showSearchButton) - QuillToolbarSearchButton( - controller: controller, - options: toolbarConfigurations.buttonOptions.search, - ), - if (customButtons.isNotEmpty) - if (showDividers) - QuillToolbarDivider( - axis, - color: sectionDividerColor, - space: sectionDividerSpace, - ), - for (final customButton in customButtons) - if (customButton.child != null) ...[ - InkWell( - onTap: customButton.onTap, - child: customButton.child, - ), - ] else ...[ - CustomButton( - onPressed: customButton.onTap, - icon: customButton.iconData ?? - context.quillToolbarBaseButtonOptions?.iconData, - iconColor: customButton.iconColor, - iconSize: customButton.iconSize ?? globalIconSize, - iconTheme: iconTheme ?? - context.quillToolbarBaseButtonOptions?.iconTheme, - afterButtonPressed: customButton.afterButtonPressed ?? - context.quillToolbarBaseButtonOptions?.afterButtonPressed, - tooltip: customButton.tooltip ?? - context.quillToolbarBaseButtonOptions?.tooltip, - ), - ], - ]; - }, - ); - } - - final QuillToolbarChildrenBuilder childrenBuilder; - final Axis axis; - final double toolbarSectionSpacing; - final WrapAlignment toolbarIconAlignment; - final WrapCrossAlignment toolbarIconCrossAlignment; - - // Overrides the action in the _LinkDialog widget - final LinkDialogAction? linkDialogAction; - - /// The color of the toolbar. - /// - /// Defaults to [ThemeData.canvasColor] of the current [Theme] if no color - /// is given. - final Color? color; - - /// List of custom buttons - final List customButtons; - - /// The color to use when painting the toolbar section divider. - /// - /// If this is null, then the [DividerThemeData.color] is used. If that is - /// also null, then [ThemeData.dividerColor] is used. - final Color? sectionDividerColor; - - /// The space occupied by toolbar section divider. - final double? sectionDividerSpace; - - /// The decoration to use for the toolbar. - final Decoration? decoration; - - // We can't get the modified [toolbarSize] by the developer - // but I tested the [QuillToolbar] on the [appBar] and I didn't notice - // a difference no matter what the value is so I will leave it to the - // default - @override - Size get preferredSize => axis == Axis.horizontal - ? const Size.fromHeight(defaultToolbarSize) - : const Size.fromWidth(defaultToolbarSize); - - @override - Widget build(BuildContext context) { - final toolbarConfigurations = context.requireQuillToolbarConfigurations; - final toolbarSize = toolbarConfigurations.toolbarSize; - return I18n( - initialLocale: context.quillSharedConfigurations?.locale, - child: (toolbarConfigurations.multiRowsDisplay) - ? Wrap( - direction: axis, - alignment: toolbarIconAlignment, - crossAxisAlignment: toolbarIconCrossAlignment, - runSpacing: 4, - spacing: toolbarSectionSpacing, - children: childrenBuilder(context), - ) - : Container( - decoration: decoration ?? - BoxDecoration( - color: color ?? Theme.of(context).canvasColor, + ), + if (configurations.showDividers && + isButtonGroupShown[1] && + (isButtonGroupShown[2] || + isButtonGroupShown[3] || + isButtonGroupShown[4] || + isButtonGroupShown[5])) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, + ), + if (configurations.showHeaderStyle) + QuillToolbarSelectHeaderStyleButtons( + controller: controller, + options: toolbarConfigurations + .buttonOptions.selectHeaderStyleButtons, + ), + if (configurations.showDividers && + configurations.showHeaderStyle && + isButtonGroupShown[2] && + (isButtonGroupShown[3] || + isButtonGroupShown[4] || + isButtonGroupShown[5])) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, + ), + if (configurations.showListNumbers) + QuillToolbarToggleStyleButton( + attribute: Attribute.ol, + options: toolbarConfigurations.buttonOptions.listNumbers, + controller: toolbarConfigurations + .buttonOptions.listNumbers.controller ?? + globalController, + ), + if (configurations.showListBullets) + QuillToolbarToggleStyleButton( + attribute: Attribute.ul, + options: toolbarConfigurations.buttonOptions.listBullets, + controller: toolbarConfigurations + .buttonOptions.listBullets.controller ?? + globalController, + ), + if (configurations.showListCheck) + QuillToolbarToggleCheckListButton( + options: toolbarConfigurations.buttonOptions.toggleCheckList, + controller: toolbarConfigurations + .buttonOptions.toggleCheckList.controller ?? + globalController, + ), + if (configurations.showCodeBlock) + QuillToolbarToggleStyleButton( + attribute: Attribute.codeBlock, + options: toolbarConfigurations.buttonOptions.codeBlock, + controller: toolbarConfigurations + .buttonOptions.codeBlock.controller ?? + globalController, + ), + if (configurations.showDividers && + isButtonGroupShown[3] && + (isButtonGroupShown[4] || isButtonGroupShown[5])) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, + ), + if (configurations.showQuote) + QuillToolbarToggleStyleButton( + options: toolbarConfigurations.buttonOptions.quote, + controller: + toolbarConfigurations.buttonOptions.quote.controller ?? + globalController, + attribute: Attribute.blockQuote, + ), + if (configurations.showIndent) + QuillToolbarIndentButton( + controller: toolbarConfigurations + .buttonOptions.indentIncrease.controller ?? + globalController, + isIncrease: true, + options: toolbarConfigurations.buttonOptions.indentIncrease, + ), + if (configurations.showIndent) + QuillToolbarIndentButton( + controller: toolbarConfigurations + .buttonOptions.indentDecrease.controller ?? + globalController, + isIncrease: false, + options: toolbarConfigurations.buttonOptions.indentDecrease, + ), + if (configurations.showDividers && + isButtonGroupShown[4] && + isButtonGroupShown[5]) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, + ), + if (configurations.showLink) + QuillToolbarLinkStyleButton( + controller: controller, + options: toolbarConfigurations.buttonOptions.linkStyle, + ), + if (configurations.showSearchButton) + QuillToolbarSearchButton( + controller: controller, + options: toolbarConfigurations.buttonOptions.search, + ), + if (configurations.customButtons.isNotEmpty) + if (configurations.showDividers) + QuillToolbarDivider( + axis, + color: configurations.sectionDividerColor, + space: configurations.sectionDividerSpace, ), - constraints: BoxConstraints.tightFor( - height: axis == Axis.horizontal ? toolbarSize : null, - width: axis == Axis.vertical ? toolbarSize : null, - ), - child: QuillToolbarArrowIndicatedButtonList( - axis: axis, - buttons: childrenBuilder(context), - ), - ), + for (final customButton in configurations.customButtons) + if (customButton.child != null) ...[ + InkWell( + onTap: customButton.onTap, + child: customButton.child, + ), + ] else ...[ + CustomButton( + onPressed: customButton.onTap, + icon: customButton.iconData ?? + context.quillToolbarBaseButtonOptions?.iconData, + iconColor: customButton.iconColor, + iconSize: customButton.iconSize ?? globalIconSize, + iconTheme: configurations.iconTheme ?? + context.quillToolbarBaseButtonOptions?.iconTheme, + afterButtonPressed: customButton.afterButtonPressed ?? + context + .quillToolbarBaseButtonOptions?.afterButtonPressed, + tooltip: customButton.tooltip ?? + context.quillToolbarBaseButtonOptions?.tooltip, + ), + ], + ]; + }, + ), + ), ); } } - -/// The divider which is used for separation of buttons in the toolbar. -/// -/// It can be used outside of this package, for example when user does not use -/// [QuillToolbar.basic] and compose toolbar's children on its own. -class QuillToolbarDivider extends StatelessWidget { - const QuillToolbarDivider( - this.axis, { - super.key, - this.color, - this.space, - }); - - /// Provides a horizontal divider for vertical toolbar. - const QuillToolbarDivider.horizontal({Color? color, double? space}) - : this(Axis.horizontal, color: color, space: space); - - /// Provides a horizontal divider for horizontal toolbar. - const QuillToolbarDivider.vertical({Color? color, double? space}) - : this(Axis.vertical, color: color, space: space); - - /// The axis along which the toolbar is. - final Axis axis; - - /// The color to use when painting this divider's line. - final Color? color; - - /// The divider's space (width or height) depending of [axis]. - final double? space; - - @override - Widget build(BuildContext context) { - // Vertical toolbar requires horizontal divider, and vice versa - return axis == Axis.vertical - ? Divider( - height: space, - color: color, - indent: 12, - endIndent: 12, - ) - : VerticalDivider( - width: space, - color: color, - indent: 12, - endIndent: 12, - ); - } -} diff --git a/lib/src/widgets/utils/provider.dart b/lib/src/widgets/utils/provider.dart index 55553cc2..ff217bba 100644 --- a/lib/src/widgets/utils/provider.dart +++ b/lib/src/widgets/utils/provider.dart @@ -61,3 +61,115 @@ class QuillProvider extends InheritedWidget { ); } } + +class QuillToolbarProvider extends InheritedWidget { + const QuillToolbarProvider({ + required super.child, + required this.toolbarConfigurations, + }); + + /// The configurations for the toolbar widget of flutter quill + final QuillToolbarConfigurations toolbarConfigurations; + + @override + bool updateShouldNotify(covariant QuillToolbarProvider oldWidget) { + return oldWidget.toolbarConfigurations != toolbarConfigurations; + } + + static QuillToolbarProvider? of(BuildContext context) { + /// The configurations for the quill editor widget of flutter quill + return context.dependOnInheritedWidgetOfExactType(); + } + + static QuillToolbarProvider ofNotNull(BuildContext context) { + final provider = of(context); + if (provider == null) { + if (kDebugMode) { + debugPrint( + 'The quill toolbar provider must be provided in the widget tree.', + ); + } + throw ArgumentError.checkNotNull( + 'You are using a widget in the Flutter quill library that require ' + 'The Quill toolbar provider widget to be in the parent widget tree ' + 'because ' + 'The provider is $provider. Please make sure to wrap this widget' + ' with' + ' QuillProvider widget. ' + 'You might using QuillToolbar so make sure to' + ' wrap them with the quill provider widget and setup the required ' + 'configurations', + 'QuillProvider', + ); + } + return provider; + } + + /// To pass the [QuillToolbarProvider] instance as value instead of creating + /// new widget + static QuillToolbarProvider value({ + required QuillToolbarProvider value, + required Widget child, + }) { + return QuillToolbarProvider( + toolbarConfigurations: value.toolbarConfigurations, + child: child, + ); + } +} + +class QuillEditorProvider extends InheritedWidget { + const QuillEditorProvider({ + required super.child, + required this.editorConfigurations, + }); + + /// The configurations for the quill editor widget of flutter quill + final QuillEditorConfigurations editorConfigurations; + + @override + bool updateShouldNotify(covariant QuillEditorProvider oldWidget) { + return oldWidget.editorConfigurations != editorConfigurations; + } + + static QuillEditorProvider? of(BuildContext context) { + /// The configurations for the quill editor widget of flutter quill + return context.dependOnInheritedWidgetOfExactType(); + } + + static QuillEditorProvider ofNotNull(BuildContext context) { + final provider = of(context); + if (provider == null) { + if (kDebugMode) { + debugPrint( + 'The quill editor provider must be provided in the widget tree.', + ); + } + throw ArgumentError.checkNotNull( + 'You are using a widget in the Flutter quill library that require ' + 'The Quill editor provider widget to be in the parent widget tree ' + 'because ' + 'The provider is $provider. Please make sure to wrap this widget' + ' with' + ' QuillProvider widget. ' + 'You might using QuillEditor so make sure to' + ' wrap them with the quill provider widget and setup the required ' + 'configurations', + 'QuillProvider', + ); + } + return provider; + } + + /// To pass the [QuillEditorProvider] instance as value instead of creating + /// new widget + static QuillEditorProvider value({ + required QuillEditorProvider value, + required Widget child, + }) { + return QuillEditorProvider( + editorConfigurations: value.editorConfigurations, + child: child, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b25bc38b..b6906df7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,10 @@ 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: 7.9.0 +version: 7.10.0 homepage: https://1o24bbs.com/c/bulletjournal/108 repository: https://github.com/singerdmx/flutter-quill topics: - ui - - effects - widgets - widget - rich-text-editor diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart index f9a8124d..6ade28f5 100644 --- a/test/bug_fix_test.dart +++ b/test/bug_fix_test.dart @@ -18,11 +18,13 @@ void main() { configurations: QuillConfigurations( controller: QuillController.basic(), ), - child: QuillToolbar.basic( - showRedo: false, - customButtons: [ - const QuillCustomButton(tooltip: tooltip), - ], + child: const QuillToolbar( + configurations: QuillToolbarConfigurations( + showRedo: false, + customButtons: [ + QuillCustomButton(tooltip: tooltip), + ], + ), ), ), ), @@ -38,7 +40,7 @@ void main() { builtinFinder.evaluate().first.widget as QuillToolbarIconButton; final customFinder = find.descendant( - of: find.byType(QuillToolbar), + of: find.byType(QuillBaseToolbar), matching: find.byWidgetPredicate((widget) => widget is QuillToolbarIconButton && widget.tooltip == tooltip), matchRoot: true); @@ -57,7 +59,11 @@ void main() { setUp(() { controller = QuillController.basic(); editor = QuillEditor.basic( - readOnly: false, + // ignore: avoid_redundant_argument_values + configurations: const QuillEditorConfigurations( + // ignore: avoid_redundant_argument_values + readOnly: false, + ), ); }); diff --git a/test/widgets/editor_test.dart b/test/widgets/editor_test.dart index b7d07e68..adaaea49 100644 --- a/test/widgets/editor_test.dart +++ b/test/widgets/editor_test.dart @@ -25,7 +25,13 @@ void main() { QuillProvider( configurations: QuillConfigurations(controller: controller), child: MaterialApp( - home: QuillEditor.basic(readOnly: false), + home: QuillEditor.basic( + // ignore: avoid_redundant_argument_values + configurations: const QuillEditorConfigurations( + // ignore: avoid_redundant_argument_values + readOnly: false, + ), + ), ), ), ); @@ -41,24 +47,21 @@ void main() { home: QuillProvider( configurations: QuillConfigurations( controller: controller, - // ignore: avoid_redundant_argument_values - editorConfigurations: const QuillEditorConfigurations( - // ignore: avoid_redundant_argument_values - readOnly: false, - ), ), child: QuillEditor( focusNode: FocusNode(), scrollController: ScrollController(), - scrollable: true, - padding: const EdgeInsets.all(0), - autoFocus: true, - expands: true, - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (content) { - latestUri = content.uri; - }, - allowedMimeTypes: const ['image/gif'], + configurations: QuillEditorConfigurations( + // ignore: avoid_redundant_argument_values + readOnly: false, + autoFocus: true, + expands: true, + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (content) { + latestUri = content.uri; + }, + allowedMimeTypes: ['image/gif'], + ), ), ), ), @@ -121,20 +124,18 @@ void main() { home: QuillProvider( configurations: QuillConfigurations( controller: controller, - // ignore: avoid_redundant_argument_values - editorConfigurations: const QuillEditorConfigurations( - // ignore: avoid_redundant_argument_values - readOnly: false, - ), ), child: QuillEditor( focusNode: FocusNode(), scrollController: ScrollController(), - scrollable: true, - padding: EdgeInsets.zero, - autoFocus: true, - expands: true, - contextMenuBuilder: customBuilder, + // ignore: avoid_redundant_argument_values + configurations: QuillEditorConfigurations( + // ignore: avoid_redundant_argument_values + readOnly: false, + autoFocus: true, + expands: true, + contextMenuBuilder: customBuilder, + ), ), ), ),